[READ-ONLY] Mirror of https://github.com/danielroe/cross-origin-storage. Load shared dependencies from Cross-Origin Storage (COS).
cross-origin-storage experimental nuxt vite vite-plugin
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 5.0 kB View raw
1import { mkdtemp, rm } from 'node:fs/promises' 2import { tmpdir } from 'node:os' 3import { join } from 'node:path' 4import { fileURLToPath } from 'node:url' 5import { afterAll, beforeAll, describe, expect, it } from 'vitest' 6import { setup, url } from '@nuxt/test-utils/e2e' 7import type { Page } from 'playwright-core' 8import { assertExtensionRunnable, launchPlainBrowser, launchWithExtension, skipExtensionTest } from './utils/browser' 9 10const cosChunkPattern = /\/_nuxt\/[a-f0-9]{64}\.js$/ 11 12// The COS extension's content scripts match `http://localhost:*` but not the 13// `127.0.0.1` host that @nuxt/test-utils binds to; both are loopback. 14function localhost(target: string): string { 15 return target.replace('127.0.0.1', 'localhost') 16} 17 18async function hydrate(page: Page): Promise<{ errors: string[], failed: string[] }> { 19 const errors: string[] = [] 20 const failed: string[] = [] 21 page.on('console', msg => msg.type() === 'error' && errors.push(msg.text())) 22 page.on('requestfailed', req => failed.push(req.url())) 23 24 await page.goto(url('/'), { waitUntil: 'networkidle' }) 25 26 const importMap = await page.locator('script[type="importmap"]').textContent() 27 expect(importMap).toMatch(/cos1:[a-f0-9]{64}/) 28 29 // Hydration only completes if the whole cos chunk graph resolved and ran. 30 await page.locator('button').click() 31 await page.locator('p', { hasText: 'count: 1' }).waitFor({ timeout: 5000 }) 32 expect(await page.locator('p').textContent()).toBe('count: 1') 33 34 return { errors, failed } 35} 36 37describe('browser', async () => { 38 await setup({ 39 rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), 40 build: true, 41 }) 42 43 describe('without the cos extension (network fallback)', () => { 44 it('hydrates the app and loads every managed chunk over the network', async () => { 45 const browser = await launchPlainBrowser() 46 try { 47 const page = await browser.newPage() 48 const cosChunks = new Set<string>() 49 page.on('response', (res) => { 50 const path = new URL(res.url()).pathname 51 if (cosChunkPattern.test(path)) { 52 expect(res.status(), `${path} returned ${res.status()}`).toBe(200) 53 cosChunks.add(path) 54 } 55 }) 56 57 const { errors, failed } = await hydrate(page) 58 59 expect(await page.evaluate(() => 'crossOriginStorage' in navigator)).toBe(false) 60 // vue + runtime-dom + runtime-core + reactivity + shared 61 expect(cosChunks.size).toBe(5) 62 expect(failed, `failed requests: ${failed.join(', ')}`).toEqual([]) 63 expect(errors, `console errors: ${errors.join(', ')}`).toEqual([]) 64 } 65 finally { 66 await browser.close() 67 } 68 }) 69 }) 70 71 // Skip only when explicitly opted out; otherwise a missing extension or 72 // browser is a setup failure and throws in beforeAll. 73 describe.skipIf(skipExtensionTest())('with the cos extension', () => { 74 let userDataDir: string 75 76 beforeAll(async () => { 77 assertExtensionRunnable() 78 userDataDir = await mkdtemp(join(tmpdir(), 'cos-ext-')) 79 }) 80 81 afterAll(async () => { 82 await rm(userDataDir, { recursive: true, force: true }) 83 }) 84 85 it('stores chunks in cos on first load, then serves them from cos without the network', async () => { 86 const context = await launchWithExtension(userDataDir) 87 try { 88 const page = await context.newPage() 89 const cosErrors: string[] = [] 90 page.on('console', (msg) => { 91 if (msg.type() === 'error' && msg.text().includes('[cos]')) cosErrors.push(msg.text()) 92 }) 93 94 // First load: cold cache, the extension injects the API and the loader 95 // stores every managed chunk in COS. 96 await page.goto(localhost(url('/')), { waitUntil: 'networkidle' }) 97 expect(await page.evaluate(() => 'crossOriginStorage' in navigator)).toBe(true) 98 99 // The store is async; give it a beat to settle. A declared hash that 100 // disagrees with the served bytes surfaces here as a COS store error. 101 await page.waitForTimeout(500) 102 expect(cosErrors, `cos errors on first load: ${cosErrors.join(' | ')}`).toEqual([]) 103 104 // Second load: chunks must come from COS, not the network. The 105 // persistent context keeps the COS store across reloads. 106 const networkCosChunks: string[] = [] 107 page.on('response', (res) => { 108 if (cosChunkPattern.test(new URL(res.url()).pathname)) { 109 networkCosChunks.push(res.url()) 110 } 111 }) 112 await page.reload({ waitUntil: 'networkidle' }) 113 114 await page.locator('button').click() 115 await page.locator('p', { hasText: 'count: 1' }).waitFor({ timeout: 5000 }) 116 expect(await page.locator('p').textContent()).toBe('count: 1') 117 118 expect(networkCosChunks, `chunks fetched from network instead of COS: ${networkCosChunks.join(', ')}`).toEqual([]) 119 } 120 finally { 121 await context.close() 122 } 123 }) 124 }) 125})