Skip to content

Commit cb44d2b

Browse files
test(themes): guard inactive Halo resources and SEO
中文:为未激活的 Halo 主题补齐资源隔离与 SEO 测试,并自动覆盖新注册主题。
1 parent 4b8d494 commit cb44d2b

3 files changed

Lines changed: 46 additions & 22 deletions

File tree

‎docs/themes.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,17 @@ establish zero shared theme-selector overhead, guarantee search rankings, or
179179
replace production Core Web Vitals measurements. The shared pre-paint colors
180180
remain intentional to avoid a flash when an opted-in theme is restored.
181181

182+
The isolation suites derive optional themes from `APP_THEMES`, so registering
183+
a new theme adds its embed and activation/teardown cases automatically.
184+
Halo is also included in the inactive-path asset and saved-preference guards.
185+
Keep artwork, font declarations, layout CSS and music behind the selection
186+
boundary. Do not add theme asset preload hints to shared layouts.
187+
The default-page checks seed saved music/sound opt-ins for every guarded theme;
188+
these preferences must not start media on light/dark pages or embeds.
189+
Crawler and theme-switch comparisons also cover robots, Open Graph and Twitter
190+
metadata. Run these tests on the implementation's final commit before merge;
191+
passing an earlier commit does not verify later changes.
192+
182193
- Direct selection of every theme from the picker, keyboard navigation,
183194
persistence after reload, and cleanup on exit.
184195
- No CS:GO image requests on a cold light/dark landing; images load only

‎packages/app/cypress/e2e/optional-theme-isolation.cy.ts‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
1+
import { APP_THEMES } from '../../src/lib/themes';
2+
3+
const optionalThemes = APP_THEMES.filter((theme) => !['light', 'dark', 'system'].includes(theme));
14
const featureCode =
2-
/THREE\.WebGLRenderer|WebGLRenderer:|LOS SANTOS 3D|minecraft-click\.mp3|ender-dragon\.mp3|Loading Luigi Circuit|inferencex-minecraft-worlds|mc-panorama-cube|\.mc-hotbar-wrap|\.csgo-scene|\.gta-scene|\.mc-dragon-flyacross|\.kart-scene|\.halo-scene|font-family:\s*["']?(?:Monocraft|Pricedown|ChaletComprime|Industry)/i;
3-
const featureAsset =
4-
/\/decorative\/(?:minecraft|csgo|gta|kart|halo)\/|minecraft-click\.mp3|Monocraft-|Pricedown|ChaletComprime|Industry-|youtube(?:-nocookie)?\.com|ytimg\.com/i;
5+
/THREE\.WebGLRenderer|WebGLRenderer:|LOS SANTOS 3D|minecraft-click\.mp3|ender-dragon\.mp3|Loading Luigi Circuit|inferencex-minecraft-worlds|mc-panorama-cube|\.mc-hotbar-wrap|\.csgo-scene|\.gta-scene|\.mc-dragon-flyacross|\.kart-scene|\.halo-scene|halo-theme\.css|font-family:\s*["']?(?:Monocraft|Pricedown|ChaletComprime|Industry|Halo)/i;
6+
const featureAsset = new RegExp(
7+
`/decorative/(?:${optionalThemes.join('|')})/|minecraft-click\\.mp3|Monocraft-|Pricedown|ChaletComprime|Industry-|halo[^/]*\\.(?:woff2?|ttf|otf|mp3|ogg|wav)|youtube(?:-nocookie)?\\.com|ytimg\\.com`,
8+
'i',
9+
);
510
const featureElements =
611
'[data-testid="minecraft-game"], [data-testid="minecraft-play-banner"], [data-testid="kart-game"], [data-testid$="-theme-banner"], .mc-dragon-flyacross';
712
const isFeatureAsset = (url: string) => featureAsset.test(decodeURIComponent(url));
813

14+
function saveMediaOptIns(win: Window, enabled: boolean) {
15+
for (const theme of optionalThemes) {
16+
win.localStorage.setItem(`${theme}-music`, String(enabled));
17+
win.localStorage.setItem(`${theme}-sound`, String(enabled));
18+
}
19+
}
20+
921
function expectNoOptionalResources(requests: string[]) {
1022
cy.window().then((win) => {
1123
// Request interception catches in-flight requests too; resource timing only lists completed ones.
1224
expect(requests.filter(isFeatureAsset), 'optional requests started').to.deep.equal([]);
1325
const resources = win.performance.getEntriesByType('resource');
1426
expect(resources.filter((entry) => isFeatureAsset(entry.name))).to.have.length(0);
1527
expect(
16-
[...win.document.fonts].filter((font) => /monocraft|pricedown|chalet/i.test(font.family)),
28+
[...win.document.fonts].filter((font) =>
29+
/monocraft|pricedown|chalet|industry|halo/i.test(font.family),
30+
),
1731
).to.have.length(0);
1832
for (const resource of resources.filter((entry) => /\.(?:js|css)(?:\?|$)/.test(entry.name)))
1933
cy.request(resource.name).its('body').should('not.match', featureCode);
2034
});
2135
cy.get(featureElements).should('not.exist');
36+
cy.get('audio, video, iframe[src*="youtube"]').should('not.exist');
2237
}
2338
const seo = (doc: Document) => [
2439
doc.title,
2540
doc.querySelector('meta[name="description"]')?.getAttribute('content'),
2641
doc.querySelector('link[rel="canonical"]')?.getAttribute('href'),
42+
...[
43+
...doc.querySelectorAll('meta[name="robots"], meta[property^="og:"], meta[name^="twitter:"]'),
44+
].map((node) => node.outerHTML),
2745
...[...doc.querySelectorAll('link[hreflang], script[type="application/ld+json"]')].map(
2846
(node) => node.outerHTML,
2947
),
@@ -50,8 +68,7 @@ describe('optional themes stay off the default page', () => {
5068
if (theme === 'default') win.localStorage.removeItem('theme');
5169
else win.localStorage.setItem('theme', theme);
5270
// Stored opt-ins must not start media outside the selected theme.
53-
win.localStorage.setItem('minecraft-music', 'true');
54-
win.localStorage.setItem('minecraft-sound', 'true');
71+
saveMediaOptIns(win, true);
5572
},
5673
});
5774
if (theme === 'light' || theme === 'dark') cy.get('html').should('have.class', theme);
@@ -71,7 +88,7 @@ describe('optional themes stay off the default page', () => {
7188
});
7289
}
7390

74-
for (const theme of ['minecraft', 'csgo', 'gta', 'kart', 'halo']) {
91+
for (const theme of optionalThemes) {
7592
for (const prefix of ['', '/zh']) {
7693
it(`${prefix || 'English'} embed ignores saved ${theme} before and after hydration`, () => {
7794
const requests: string[] = [];
@@ -81,9 +98,7 @@ describe('optional themes stay off the default page', () => {
8198
cy.visit(`${prefix}/embed/model/deepseek-v4?theme=light`, {
8299
onBeforeLoad(win) {
83100
win.localStorage.setItem('theme', theme);
84-
win.localStorage.setItem('minecraft-music', 'true');
85-
win.localStorage.setItem('minecraft-sound', 'true');
86-
win.localStorage.setItem('halo-music', 'true');
101+
saveMediaOptIns(win, true);
87102
},
88103
});
89104
cy.get('html').should('have.class', 'light').and('have.attr', 'data-inferencex-embed');
@@ -116,11 +131,7 @@ describe('optional themes stay off the default page', () => {
116131
(link) => isFeatureAsset(link.getAttribute('href') ?? ''),
117132
),
118133
).to.have.length(0);
119-
expect(
120-
doc.querySelector(
121-
'[data-testid="kart-game"], [data-testid="gta-theme-banner"], [data-testid="csgo-theme-banner"], [data-testid="halo-theme-banner"]',
122-
),
123-
).to.equal(null);
134+
expect(doc.querySelector(featureElements)).to.equal(null);
124135
// Metadata and indexable headings are in raw HTML, without running JavaScript.
125136
cy.request(route).then((normal) => {
126137
const normalDoc = new DOMParser().parseFromString(normal.body, 'text/html');
@@ -135,9 +146,7 @@ describe('optional themes stay off the default page', () => {
135146
cy.visit('/about', {
136147
onBeforeLoad(win) {
137148
win.localStorage.setItem('theme', 'light');
138-
win.localStorage.setItem('minecraft-music', 'false');
139-
win.localStorage.setItem('minecraft-sound', 'false');
140-
win.localStorage.setItem('halo-music', 'false');
149+
saveMediaOptIns(win, false);
141150
},
142151
});
143152
// The server-rendered trigger is visible before its click handler hydrates.
@@ -146,7 +155,7 @@ describe('optional themes stay off the default page', () => {
146155
.and('contain', 'currently');
147156
cy.document().then((doc) => {
148157
const baseline = seo(doc);
149-
for (const theme of ['csgo', 'gta', 'minecraft', 'kart', 'halo']) {
158+
for (const theme of optionalThemes) {
150159
cy.get('[data-testid="theme-toggle"]').click();
151160
cy.get(`[data-testid="theme-option-${theme}"]`).click();
152161
if (theme === 'minecraft') cy.get('canvas').should('exist');

‎packages/app/src/lib/optional-theme-imports.test.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
22
import path from 'node:path';
33
import ts from 'typescript';
44
import { describe, expect, it } from 'vitest';
5+
import { APP_THEMES } from './themes';
56

67
const src = path.resolve(import.meta.dirname, '..');
7-
const optionalDirectory = /^components\/(?:minecraft|csgo|gta|kart|halo)\//;
8+
const optionalThemes = APP_THEMES.filter((theme) => !['light', 'dark', 'system'].includes(theme));
9+
const optionalDirectory = new RegExp(`^components/(?:${optionalThemes.join('|')})/`);
810
// These wrappers contain only activation gates and dynamic imports.
911
const wrappers = new Set([
1012
'components/minecraft/minecraft-splash.tsx',
1113
'components/minecraft/minecraft-toggles-lazy.tsx',
1214
'components/halo/halo-toggles-lazy.tsx',
1315
]);
1416
const engine = /^(?:three|@react-three\/[^/]+|@dimforge\/rapier[^/]*)(?:\/|$)/;
15-
const optionalAsset =
16-
/(?:\/decorative\/(?:minecraft|csgo|gta|kart|halo)\/|minecraft-click\.mp3|Monocraft-|Pricedown|ChaletComprime|Industry-)/i;
17+
const optionalAsset = new RegExp(
18+
`(?:/decorative/(?:${optionalThemes.join('|')})/|minecraft-click\\.mp3|Monocraft-|Pricedown|ChaletComprime|Industry-|halo[^/]*\\.(?:woff2?|ttf|otf|mp3|ogg|wav))`,
19+
'i',
20+
);
1721

1822
/** Static runtime imports, including re-exports. Dynamic imports stay behind their gate. */
1923
function imports(text: string): string[] {

0 commit comments

Comments
 (0)