diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ad44b2..1b9755c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: ember-source: specifier: '>= 3.28' version: 5.12.0(@glimmer/component@2.1.1)(@glint/template@1.7.4)(rsvp@4.8.5)(webpack@5.104.1) + should-handle-link: + specifier: ^1.2.2 + version: 1.3.0 devDependencies: '@babel/core': specifier: ^7.23.6 @@ -8421,6 +8424,7 @@ snapshots: '@embroider/addon-shim': 1.10.2 ember-source: 7.1.0(@glimmer/component@2.1.1) qunit: 2.25.0 + should-handle-link: 1.3.0 transitivePeerDependencies: - '@babel/core' - '@glint/template' diff --git a/test-app/app/templates/application.gts b/test-app/app/templates/application.gts index 8ac3fb7..a8daeb3 100644 --- a/test-app/app/templates/application.gts +++ b/test-app/app/templates/application.gts @@ -7,4 +7,10 @@ here here / here + + {{! non-SPA links: handled by the browser, not the router — visitAllLinks must skip them }} + new tab + download + external + email diff --git a/test-app/tests/acceptance/visit-all-links-test.ts b/test-app/tests/acceptance/visit-all-links-test.ts index 161803e..1bfa216 100644 --- a/test-app/tests/acceptance/visit-all-links-test.ts +++ b/test-app/tests/acceptance/visit-all-links-test.ts @@ -20,4 +20,23 @@ module('All Links', function (hooks) { `multiple usages does not visit different numbers of links (${size1} === ${size2})`, ); }); + + test('non-SPA links are skipped', async function (assert) { + // The application template renders target="_blank", download, + // rel="external", and mailto: links. The browser handles those natively — + // clicking them can never change currentURL() — so a crawl that clicked + // them would report failed navigations. Every URL the crawl does visit + // must be one the router can serve. + const visited: string[] = []; + + await visitAllLinks((url) => { + visited.push(url); + + const isNonSPA = url.startsWith('mailto:') || url.endsWith('.html'); + + assert.false(isNonSPA, `${url} is a route the router handles`); + }); + + assert.ok(visited.length > 0, 'the SPA links were still visited'); + }); }); diff --git a/test-support/package.json b/test-support/package.json index 1288cdd..25c89e3 100644 --- a/test-support/package.json +++ b/test-support/package.json @@ -53,7 +53,8 @@ }, "dependencies": { "@ember/test-helpers": "^4.0.4 || ^5.2.2", - "@embroider/addon-shim": "^1.8.7" + "@embroider/addon-shim": "^1.8.7", + "should-handle-link": "^1.2.2" }, "devDependencies": { "@babel/core": "^7.23.6", diff --git a/test-support/src/routing/visit-all.ts b/test-support/src/routing/visit-all.ts index 843d2e7..2a1febd 100644 --- a/test-support/src/routing/visit-all.ts +++ b/test-support/src/routing/visit-all.ts @@ -8,6 +8,7 @@ import { findAll, } from '@ember/test-helpers'; import QUnit from 'qunit'; +import { shouldHandle } from 'should-handle-link'; import type Owner from '@ember/owner'; import type RouterService from '@ember/routing/router-service'; @@ -23,10 +24,29 @@ function findInAppLinks(): InAppLink[] { const allAnchorsOnThePage = findAll('a'); for (const a of allAnchorsOnThePage) { + // `findAll('a')` can also match SVG ``, whose `href` is an + // SVGAnimatedString the router (and shouldHandle) can't work with. + if (!(a instanceof HTMLAnchorElement)) continue; + const href = a.getAttribute('href'); if (!href) continue; - if (href.startsWith('http')) continue; + + /** + * Links the SPA's router never handles are handled natively by the + * browser instead (new tab/window via `target`, download dialog, + * `mailto:`/`tel:`, cross-origin, `rel="external"`). Clicking them in a + * test can't change `currentURL()`, so they'd always be reported as + * failed navigations. + * + * `shouldHandle` is the predicate ember-primitives' @properLinks uses to + * decide whether the router handles a click, so the crawler visits + * exactly the set of links the router would. The fabricated click event + * carries the "plain left click" defaults (button 0, no modifier keys). + */ + if (!shouldHandle(window.location.href, a, new MouseEvent('click'))) { + continue; + } const current = new URL(currentURL(), window.location.origin); const url = new URL(href, current);