Skip to content

Commit 6bd21e9

Browse files
feat: improve link checker audit workflow
1 parent 1eb7897 commit 6bd21e9

15 files changed

Lines changed: 2160 additions & 315 deletions

apps/link-checker/__tests__/locations/ConfigScreen.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ describe('Config Screen component', () => {
3333
expect(getByText(/^Deny list$/)).toBeInTheDocument();
3434
expect(getByText('Disclaimer')).toBeInTheDocument();
3535
expect(getByPlaceholderText('https://www.example.com')).toBeInTheDocument();
36-
expect(getByPlaceholderText(/help\.example\.com/)).toBeInTheDocument();
37-
expect(getByPlaceholderText(/staging\.example\.com/)).toBeInTheDocument();
36+
expect(getByPlaceholderText('Add allowed domain...')).toBeInTheDocument();
37+
expect(getByPlaceholderText('Add blocked domain...')).toBeInTheDocument();
3838
});
3939

4040
it('calls onConfigure and returns parameters and targetState', async () => {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
import { fireEvent, render, screen } from '@testing-library/react';
3+
import { mockSdk } from '../mocks';
4+
import Page from '@/components/locations/Page';
5+
6+
jest.mock('@contentful/react-apps-toolkit', () => ({
7+
useSDK: () => mockSdk,
8+
}));
9+
10+
describe('Page component', () => {
11+
beforeEach(() => {
12+
mockSdk.parameters.installation = {
13+
selectedContentTypeIds: ['article'],
14+
};
15+
mockSdk.app.getCurrentState.mockResolvedValue({
16+
EditorInterface: {
17+
article: { sidebar: { position: 1 } },
18+
},
19+
});
20+
mockSdk.cma = {
21+
contentType: {
22+
getMany: jest.fn().mockResolvedValue({
23+
items: [
24+
{
25+
sys: { id: 'article' },
26+
name: 'Article',
27+
displayField: 'title',
28+
fields: [
29+
{ id: 'title', name: 'Title', type: 'Symbol' },
30+
{ id: 'body', name: 'Body', type: 'Text' },
31+
],
32+
},
33+
],
34+
}),
35+
},
36+
entry: {
37+
getMany: jest.fn().mockResolvedValue({
38+
items: [
39+
{
40+
sys: {
41+
id: 'entry-1',
42+
contentType: { sys: { id: 'article' } },
43+
},
44+
fields: {
45+
title: { 'en-US': 'Release Notes' },
46+
body: { 'en-US': 'Visit https://example.invalid/not-found' },
47+
},
48+
},
49+
],
50+
}),
51+
},
52+
appAction: {
53+
getMany: jest.fn().mockResolvedValue({
54+
items: [
55+
{
56+
sys: {
57+
id: 'check-link-action',
58+
appDefinition: { sys: { id: mockSdk.ids.app } },
59+
},
60+
function: { sys: { id: 'checkLink' } },
61+
},
62+
],
63+
}),
64+
},
65+
appActionCall: {
66+
createWithResponse: jest.fn().mockResolvedValue({
67+
response: { body: JSON.stringify({ status: 404 }) },
68+
}),
69+
},
70+
};
71+
});
72+
73+
it('renders scanned links in the page table', async () => {
74+
render(<Page />);
75+
fireEvent.click(screen.getByRole('button', { name: 'Run scan' }));
76+
77+
await screen.findByText('https://example.invalid/not-found');
78+
});
79+
});

apps/link-checker/__tests__/locations/Sidebar.spec.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,57 @@ describe('Sidebar component', () => {
7575
await screen.findByText(/not on allow list/i);
7676
expect(createWithResponse).not.toHaveBeenCalled();
7777
});
78+
79+
it('shows only non-invalid results when remaining links are expanded', async () => {
80+
const createWithResponse = jest
81+
.fn()
82+
.mockResolvedValueOnce({ response: { body: JSON.stringify({ status: 404 }) } })
83+
.mockResolvedValueOnce({ response: { body: JSON.stringify({ status: 200 }) } });
84+
85+
mockSdk.entry.fields = {
86+
body: {
87+
id: 'body',
88+
name: 'Body',
89+
type: 'Text',
90+
locales: ['en-US'],
91+
getValue: () =>
92+
'Broken https://broken.example.com and valid https://www.contentful.com/help',
93+
},
94+
};
95+
mockSdk.cma = {
96+
appAction: {
97+
getMany: jest.fn().mockResolvedValue({
98+
items: [
99+
{
100+
sys: {
101+
id: 'check-link-action',
102+
appDefinition: { sys: { id: mockSdk.ids.app } },
103+
},
104+
function: { sys: { id: 'checkLink' } },
105+
},
106+
],
107+
}),
108+
},
109+
appActionCall: {
110+
createWithResponse,
111+
},
112+
};
113+
114+
render(<Sidebar />);
115+
116+
await act(async () => {
117+
fireEvent.click(screen.getByRole('button', { name: /check links/i }));
118+
});
119+
120+
await screen.findByText(/1 invalid link/i);
121+
expect(screen.getByRole('checkbox', { name: /show valid links/i })).toBeInTheDocument();
122+
expect(screen.getByText(/1 valid link/i)).toBeInTheDocument();
123+
124+
await act(async () => {
125+
fireEvent.click(screen.getByRole('checkbox', { name: /show valid links/i }));
126+
});
127+
128+
expect(screen.getByText('https://www.contentful.com/help')).toBeInTheDocument();
129+
expect(screen.queryAllByText('https://broken.example.com')).toHaveLength(1);
130+
});
78131
});

apps/link-checker/__tests__/mocks/mockSdk.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@ const mockSdk: any = {
1414
parameters: {
1515
installation: {},
1616
},
17+
locales: {
18+
default: 'en-US',
19+
},
1720
entry: {
1821
fields: {},
1922
},
23+
navigator: {
24+
openEntry: jest.fn(),
25+
openAppConfig: jest.fn(),
26+
},
2027
cma: {},
2128
location: { is: jest.fn().mockReturnValue(false) },
2229
};

apps/link-checker/__tests__/utils/extractUrls.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@ describe('extractUrlsFromEntry', () => {
105105
expect(result).toHaveLength(1);
106106
});
107107

108+
it('does not treat plain email addresses as URLs', () => {
109+
const entry = {
110+
fields: {
111+
replyToEmail: {
112+
id: 'replyToEmail',
113+
name: 'Reply-to email',
114+
type: 'Symbol',
115+
locales: ['en-US'],
116+
getValue: () => 'hello@colorful.com',
117+
},
118+
},
119+
};
120+
121+
expect(extractUrlsFromEntry(entry)).toEqual([]);
122+
});
123+
108124
it('includes Rich Text hyperlink URIs', () => {
109125
const entry = {
110126
fields: {
@@ -235,5 +251,21 @@ describe('extractUrlsFromEntry', () => {
235251
};
236252
expect(extractUrlsFromEntry(entry)).toEqual([]);
237253
});
254+
255+
it('does not treat a standalone slash separator as a relative link', () => {
256+
const entry = {
257+
fields: {
258+
title: {
259+
id: 'title',
260+
name: 'Title',
261+
type: 'Symbol',
262+
locales: ['en-US'],
263+
getValue: () => 'Partners / More customers',
264+
},
265+
},
266+
};
267+
268+
expect(extractUrlsFromEntry(entry)).toEqual([]);
269+
});
238270
});
239271
});

0 commit comments

Comments
 (0)