Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test-app/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class Router extends EmberRouter {
Router.map(function () {
this.route('foo');
this.route('hash-target');
this.route('inert-demo-target');
this.route('docs', function () {
this.route('page');
this.route('other');
Expand Down
18 changes: 18 additions & 0 deletions test-app/app/routes/inert-demo-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Route from '@ember/routing/route';

import type Transition from '@ember/routing/transition';

/**
* Stands in for a route an app deliberately refuses to render — the pattern
* docs sites use so a demo's links look real while clicking one keeps the
* reader in place.
*
* Such a target isn't meaningfully visitable, and in a real app an aborted
* transition can make `visit()` reject outright (TransitionAborted), failing
* the crawl. `shouldVisit` is how an app excludes them.
*/
export default class InertDemoTargetRoute extends Route {
beforeModel(transition: Transition) {
transition.abort();
}
}
3 changes: 3 additions & 0 deletions test-app/app/templates/application.gts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
{{! a page+hash link: currentURL() after the click has no #hash, so the
navigation assertion must compare without it }}
<a href="/hash-target#down">hash link</a>
{{! a link the app deliberately refuses to navigate (its route aborts the
transition) — crawls must be able to skip it via shouldVisit }}
<a href="/inert-demo-target">inert demo link</a>

{{! non-SPA links: handled by the browser, not the router — visitAllLinks must skip them }}
<a href="/foo" target="_blank" rel="noopener noreferrer">new tab</a>
Expand Down
109 changes: 88 additions & 21 deletions test-app/tests/acceptance/visit-all-links-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,29 @@ import { setupApplicationTest } from 'ember-qunit';

import { visitAllLinks } from '@universal-ember/test-support';

/**
* The application template links /inert-demo-target, whose route aborts the
* transition on purpose (the pattern docs sites use so a demo's links look
* real while clicking one keeps the reader in place). It is not meaningfully
* visitable, so every crawl here skips it — which is what `shouldVisit` is
* for.
*/
const SKIP_INERT = {
shouldVisit: (url: string) => !url.startsWith('/inert-demo-target'),
};

module('All Links', function (hooks) {
setupApplicationTest(hooks);

test('are visitable without error', async function (assert) {
const size1 = await visitAllLinks();
const size2 = await visitAllLinks((url) => {
assert.ok(url);
});
const size1 = await visitAllLinks(undefined, undefined, SKIP_INERT);
const size2 = await visitAllLinks(
(url) => {
assert.ok(url);
},
undefined,
SKIP_INERT,
);

assert.ok(size1 > 0, 'The test app has links');
assert.ok(size2 > 0, 'The test app has links');
Expand All @@ -29,13 +44,17 @@ module('All Links', function (hooks) {
// must be one the router can serve.
const visited: string[] = [];

await visitAllLinks((url) => {
visited.push(url);
await visitAllLinks(
(url) => {
visited.push(url);

const isNonSPA = url.startsWith('mailto:') || url.endsWith('.html');
const isNonSPA = url.startsWith('mailto:') || url.endsWith('.html');

assert.false(isNonSPA, `${url} is a route the router handles`);
});
assert.false(isNonSPA, `${url} is a route the router handles`);
},
undefined,
SKIP_INERT,
);

assert.ok(visited.length > 0, 'the SPA links were still visited');
});
Expand All @@ -55,14 +74,21 @@ module('All Links', function (hooks) {
console.warn = (...args: unknown[]) => warnings.push(args.join(' '));

try {
await visitAllLinks((url) => {
visited.push(url);
});
await visitAllLinks(
(url) => {
visited.push(url);
},
undefined,
SKIP_INERT,
);
} finally {
console.warn = originalWarn;
}

assert.true(visited.includes('/docs/other'), `visited: ${visited.join(', ')}`);
assert.true(
visited.includes('/docs/other'),
`visited: ${visited.join(', ')}`,
);

const warning = warnings.find((w) => w.includes('Relative href "other"'));

Expand All @@ -81,18 +107,26 @@ module('All Links', function (hooks) {
const viaVisit: string[] = [];
const viaClick: string[] = [];

await visitAllLinks((url) => {
viaVisit.push(url);
});
await visitAllLinks(
(url) => {
viaVisit.push(url);
},
undefined,
SKIP_INERT,
);
await visitAllLinks(
(url) => {
viaClick.push(url);
},
undefined,
{ mode: 'click' },
{ ...SKIP_INERT, mode: 'click' },
);

assert.deepEqual(viaClick.sort(), viaVisit.sort(), 'both modes crawl the same URLs');
assert.deepEqual(
viaClick.sort(),
viaVisit.sort(),
'both modes crawl the same URLs',
);
});

test('each target is visited once', async function (assert) {
Expand All @@ -101,9 +135,13 @@ module('All Links', function (hooks) {
// every page, and pair-keying makes the crawl quadratic in app size.
const visited: string[] = [];

await visitAllLinks((url) => {
visited.push(url);
});
await visitAllLinks(
(url) => {
visited.push(url);
},
undefined,
SKIP_INERT,
);

const paths = visited.map((url) => url.split('#')[0]);

Expand All @@ -113,4 +151,33 @@ module('All Links', function (hooks) {
`no target is visited twice: ${paths.join(', ')}`,
);
});

test('shouldVisit filters what the crawl reaches', async function (assert) {
const visited: string[] = [];

await visitAllLinks(
(url) => {
visited.push(url);
},
undefined,
SKIP_INERT,
);

assert.false(
visited.some((url) => url.startsWith('/inert-demo-target')),
'the filtered-out target was never visited',
);
assert.true(visited.length > 0, 'everything else still was');

// Filtering everything out crawls nothing (and asserts nothing).
const size = await visitAllLinks(undefined, undefined, {
shouldVisit: () => false,
});

assert.strictEqual(
size,
0,
'a filter that rejects everything visits nothing',
);
});
});
21 changes: 21 additions & 0 deletions test-support/src/routing/visit-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ interface VisitAllLinksOptions {
* apps that need per-link click fidelity.
*/
mode?: 'visit' | 'click';

/**
* Decides whether a discovered target is crawled at all. Return `false` to
* skip it — the crawl neither navigates to it nor asserts on it, and its
* own links are not discovered through it.
*
* For links the app deliberately refuses to navigate — e.g. demo links
* whose route calls `transition.abort()`, or areas under test elsewhere:
*
* ```js
* await visitAllLinks(undefined, undefined, {
* shouldVisit: (url) => !url.startsWith('/demo-targets/'),
* });
* ```
*
* Receives the app-relative target (hash included, as authored).
*/
shouldVisit?: (url: string) => boolean;
}

export async function visitAllLinks(
Expand All @@ -85,6 +103,7 @@ export async function visitAllLinks(
options?: VisitAllLinksOptions,
) {
const mode = options?.mode ?? 'visit';
const shouldVisit = options?.shouldVisit ?? (() => true);
/**
* app-relative target paths (without hash)
*/
Expand Down Expand Up @@ -136,6 +155,8 @@ export async function visitAllLinks(

if (visited.has(key)) continue;

if (!shouldVisit(toVisit.href)) continue;

const result = router.recognize(toVisit.href);

if (!result) {
Expand Down
Loading