Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"private": true,
"type": "module",
"scripts": {
"test": "run-s -c test:unit \"test:build {@}\" \"test:serve {@}\" --",
"test": "run-s -c test:unit \"test:build {@}\" \"test:serve {@}\" \"test:build:watch {@}\" --",
"test:unit": "vitest run",
"test:serve": "vitest run -c vitest.config.e2e.ts",
"test:build": "cross-env TEST_BUILD=1 vitest run -c vitest.config.e2e.ts",
"test:build:watch": "cross-env TEST_BUILD_WATCH=1 vitest run -c vitest.config.e2e.ts",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build --watch was not tested at all before but 2 recent issues highlighted that we can't just depend on it being tested by vite itself

"check": "run-p -c check:*",
"check:audit": "pnpm audit --prod",
"check:publint": "pnpm --filter \"./packages/*\" --parallel check:publint",
Expand Down
20 changes: 15 additions & 5 deletions packages/e2e-tests/_test_dependencies/vite-plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import path from 'node:path';
import fs from 'node:fs';
import MagicString from 'magic-string';

function replaceWithSourceMap(code, value, replacement) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the missing sourcemap lead to error logs, add them to allow easier debugging and assertions that no unexpected errors happened

const s = new MagicString(code);
s.replaceAll(value, replacement);
return {
code: s.toString(),
map: s.generateMap({ hires: 'boundary' })
};
}
/**
* Ensure transform flow is not interrupted
* @returns {import('vite').Plugin[]}
Expand All @@ -11,19 +21,19 @@ export function transformValidation() {
enforce: 'pre',
transform(code, id) {
if (id.endsWith('.svelte')) {
return code.replaceAll('__JS_TRANSFORM_1__', '__JS_TRANSFORM_2__');
return replaceWithSourceMap(code, '__JS_TRANSFORM_1__', '__JS_TRANSFORM_2__');
} else if (id.endsWith('.css')) {
return code.replaceAll('__CSS_TRANSFORM_1__', '__CSS_TRANSFORM_2__');
return replaceWithSourceMap(code, '__CSS_TRANSFORM_1__', '__CSS_TRANSFORM_2__');
}
}
},
{
name: 'transform-validation:2',
transform(code, id) {
if (id.endsWith('.svelte')) {
return code.replaceAll('__JS_TRANSFORM_2__', '__JS_TRANSFORM_3__');
return replaceWithSourceMap(code, '__JS_TRANSFORM_2__', '__JS_TRANSFORM_3__');
} else if (id.endsWith('.css')) {
return code.replaceAll('__CSS_TRANSFORM_2__', 'red');
return replaceWithSourceMap(code, '__CSS_TRANSFORM_2__', 'red');
}
}
},
Expand All @@ -32,7 +42,7 @@ export function transformValidation() {
enforce: 'post',
transform(code, id) {
if (id.endsWith('.svelte')) {
return code.replaceAll('__JS_TRANSFORM_3__', 'Hello world');
replaceWithSourceMap(code, '__JS_TRANSFORM_3__', 'Hello world');
}
// can't handle css here as in build, it would be `export default {}`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"main": "./index.js",
"files": [
"index.js"
]
],
"dependencies": {
"magic-string": "^0.30.17"
}
}
212 changes: 212 additions & 0 deletions packages/e2e-tests/build-watch/__tests__/build-watch.spec.ts
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is basically a copy of the hmr test, with some changed asserts (state cannot be preserved, manual page reloads more often)

Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
isBuildWatch,
getEl,
getText,
editFileAndWaitForBuildWatchComplete,
hmrCount,
untilMatches,
sleep,
getColor,
editFile,
addFile,
removeFile,
editViteConfig,
browserLogs,
waitForBuildWatchAndPageReload
} from '~utils';

test('should render App', async () => {
expect(await getText('#app-header')).toBe('Test-App');
});

test('should render static import', async () => {
expect(await getText('#static-import .label')).toBe('static-import');
});

test('should render dependency import', async () => {
expect(await getText('#dependency-import .label')).toBe('dependency-import');
});

test('should render dynamic import', async () => {
expect(await getEl('#dynamic-import')).toBe(null);
const dynamicImportButton = await getEl('#button-import-dynamic');
expect(dynamicImportButton).toBeDefined();
await dynamicImportButton.click();
await untilMatches(
() => getText('#dynamic-import .label'),
'dynamic-import',
'dynamic import loaded after click'
);
});

test('should not have failed requests', async () => {
browserLogs.forEach((msg) => {
expect(msg).not.toMatch('404');
});
});

test('should respect transforms', async () => {
expect(await getText('#js-transform')).toBe('Hello world');
expect(await getColor('#css-transform')).toBe('red');
});

if (isBuildWatch) {
describe('build --watch', () => {
const updateHmrTest = editFileAndWaitForBuildWatchComplete.bind(
null,
'src/components/HmrTest.svelte'
);
const updateModuleContext = editFileAndWaitForBuildWatchComplete.bind(
null,
'src/components/partial-hmr/ModuleContext.svelte'
);
const updateApp = editFileAndWaitForBuildWatchComplete.bind(null, 'src/App.svelte');
const updateStore = editFileAndWaitForBuildWatchComplete.bind(null, 'src/stores/hmr-stores.js');

test('should have expected initial state', async () => {
// initial state, both counters 0, both labels red
expect(await getText('#hmr-test-1 .counter')).toBe('0');
expect(await getText('#hmr-test-2 .counter')).toBe('0');
expect(await getText('#hmr-test-1 .label')).toBe('hmr-test');
expect(await getText('#hmr-test-2 .label')).toBe('hmr-test');
expect(await getColor('#hmr-test-1 .label')).toBe('red');
expect(await getColor('#hmr-test-2 .label')).toBe('red');
});

test('should have working increment button', async () => {
// increment counter of one instance to have local state to verify after build updates
await (await getEl('#hmr-test-1 .increment')).click();
await sleep(50);

// counter1 = 1, counter2 = 0
expect(await getText('#hmr-test-1 .counter')).toBe('1');
expect(await getText('#hmr-test-2 .counter')).toBe('0');
});

test('should apply css changes in HmrTest.svelte', async () => {
// update style, change label color from red to green
await updateHmrTest((content) => content.replace('color: red', 'color: green'));

// color should have changed
expect(await getColor('#hmr-test-1 .label')).toBe('green');
expect(await getColor('#hmr-test-2 .label')).toBe('green');
});

test('should apply js change in HmrTest.svelte ', async () => {
// update script, change label value
await updateHmrTest((content) =>
content.replace("const label = 'hmr-test'", "const label = 'hmr-test-updated'")
);
expect(await getText('#hmr-test-1 .label')).toBe('hmr-test-updated');
expect(await getText('#hmr-test-2 .label')).toBe('hmr-test-updated');
});

test('should reset state of external store used by HmrTest.svelte when editing App.svelte', async () => {
// update App, add a new instance of HmrTest
await updateApp((content) =>
content.replace(
'<!-- HMR-TEMPLATE-INJECT -->',
'<HmrTest id="hmr-test-3"/>\n<!-- HMR-TEMPLATE-INJECT -->'
)
);
// counter state is reset
expect(await getText('#hmr-test-1 .counter')).toBe('0');
expect(await getText('#hmr-test-2 .counter')).toBe('0');
// a third instance has been added
expect(await getText('#hmr-test-3 .counter')).toBe('0');
});

test('should reset state of store when editing hmr-stores.js', async () => {
// change state
await (await getEl('#hmr-test-2 .increment')).click();
await sleep(50);
expect(await getText('#hmr-test-2 .counter')).toBe('1');
await updateStore((content) => `${content}\n/*trigger change*/\n`);
// counter state is reset
expect(await getText('#hmr-test-2 .counter')).toBe('0');
});

test('should work when editing script context="module"', async () => {
expect(await getText('#hmr-with-context')).toContain('x=0 y=1 slot=1');
expect(await getText('#hmr-without-context')).toContain('x=0 y=1 slot=');
expect(hmrCount('UsingNamed.svelte'), 'updates for UsingNamed.svelte').toBe(0);
expect(hmrCount('UsingDefault.svelte'), 'updates for UsingDefault.svelte').toBe(0);
await updateModuleContext((content) => content.replace('y = 1', 'y = 2'));
expect(await getText('#hmr-with-context')).toContain('x=0 y=2 slot=2');
expect(await getText('#hmr-without-context')).toContain('x=0 y=2 slot=');
expect(hmrCount('UsingNamed.svelte'), 'updates for UsingNamed.svelte').toBe(1);
expect(hmrCount('UsingDefault.svelte'), 'updates for UsingDefault.svelte').toBe(0);
});

test('should work with emitCss: false in vite config', async () => {
await editViteConfig((c) => c.replace('svelte()', 'svelte({emitCss:false})'));
expect(await getText('#hmr-test-1 .counter')).toBe('0');
expect(await getColor('#hmr-test-1 .label')).toBe('green');
await (await getEl('#hmr-test-1 .increment')).click();
await sleep(50);
expect(await getText('#hmr-test-1 .counter')).toBe('1');
await updateHmrTest((content) => content.replace('color: green', 'color: red'));
expect(await getColor('#hmr-test-1 .label')).toBe('red');
// counter value is reset
expect(await getText('#hmr-test-1 .counter')).toBe('0');
});

test('should work with emitCss: false in svelte config', async () => {
addFile('svelte.config.js', 'export default {vitePlugin:{emitCss:false}}');
await waitForBuildWatchAndPageReload();
expect(await getColor('#hmr-test-1 .label')).toBe('red');
removeFile('svelte.config.js');
await waitForBuildWatchAndPageReload();
});

test('should detect changes in svelte config and rebuild', async () => {
const injectPreprocessor = ({ content, filename }) => {
if (filename && filename.includes('App.svelte')) {
return {
code: content.replace(
'<!-- HMR-TEMPLATE-INJECT -->',
'<div id="preprocess-inject">Injected</div>\n<!-- HMR-TEMPLATE-INJECT -->'
)
};
}
};
await addFile(
'svelte.config.js',
`export default {
preprocess:[{markup:${injectPreprocessor.toString()}}]};`
);
await waitForBuildWatchAndPageReload();
expect(await getText('#preprocess-inject')).toBe('Injected');
expect(await getText('#hmr-test-1 .counter')).toBe('0');
expect(await getColor('#hmr-test-1 .label')).toBe('red');
await (await getEl('#hmr-test-1 .increment')).click();
await sleep(50);
expect(await getText('#hmr-test-1 .counter')).toBe('1');
await updateHmrTest((content) => content.replace('color: red', 'color: green'));
expect(await getColor('#hmr-test-1 .label')).toBe('green');
expect(await getText('#hmr-test-1 .counter')).toBe('0');
await editFile('svelte.config.js', (content) =>
content
.replace('preprocess-inject', 'preprocess-inject-2')
.replace('Injected', 'Injected 2')
);
await waitForBuildWatchAndPageReload();
expect(await getText('#preprocess-inject-2')).toBe('Injected 2');
expect(await getEl('#preprocess-inject')).toBe(null);
expect(await getColor('#hmr-test-1 .label')).toBe('green');
expect(await getText('#hmr-test-1 .counter')).toBe('0');
await (await getEl('#hmr-test-1 .increment')).click();
await sleep(50);
expect(await getText('#hmr-test-1 .counter')).toBe('1');
await updateHmrTest((content) => content.replace('color: green', 'color: red'));
expect(await getColor('#hmr-test-1 .label')).toBe('red');
expect(await getText('#hmr-test-1 .counter')).toBe('0');
await removeFile('svelte.config.js');
await waitForBuildWatchAndPageReload();
expect(await getEl('#preprocess-inject-2')).toBe(null);
expect(await getEl('#preprocess-inject')).toBe(null);
expect(await getColor('#hmr-test-1 .label')).toBe('red');
expect(await getText('#hmr-test-1 .counter')).toBe('0');
});
});
}
12 changes: 12 additions & 0 deletions packages/e2e-tests/build-watch/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte App</title>
</head>
<body>
<script type="module" src="/src/index.js"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions packages/e2e-tests/build-watch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "e2e-tests-build-watch",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "workspace:^",
"e2e-test-dep-vite-plugins": "file:../_test_dependencies/vite-plugins",
"node-fetch": "^3.3.2",
"svelte": "^5.36.13",
"vite": "^7.0.5"
},
"type": "module"
}
Binary file added packages/e2e-tests/build-watch/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions packages/e2e-tests/build-watch/src/App.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script>
import StaticImport from './components/StaticImport.svelte';
import Dependency from 'e2e-test-dep-svelte-simple';
import HmrTest from './components/HmrTest.svelte';
import PartialHmr from './components/partial-hmr/PartialHmr.svelte';
const jsTransform = '__JS_TRANSFORM_1__';
let dynamicImportComponent;
function importDynamic() {
import('./components/DynamicImport.svelte').then((m) => (dynamicImportComponent = m.default));
}
</script>

<h1 id="app-header">Test-App 2</h1>
<!-- to be transformed into "Hello world!" text -->
<p id="js-transform">{jsTransform}</p>
<!-- to be transformed into "hello-world" class -->
<p id="css-transform">Hello world</p>
<StaticImport />
<Dependency />
{#if !dynamicImportComponent}
<button id="button-import-dynamic" on:click={importDynamic}>import dynamic component</button>
{:else}
<svelte:component this={dynamicImportComponent} />
{/if}
<HmrTest id="hmr-test-1" />
<HmrTest id="hmr-test-2" />

<!-- HMR-TEMPLATE-INJECT -->

<PartialHmr />

<style>
h1 {
color: #111111;
}

:global(#css-transform) {
color: __CSS_TRANSFORM_1__;
}
</style>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions packages/e2e-tests/build-watch/src/components/DynamicImport.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script>
import asset from '/src/assets/dynamic.png';
const importedAsset = asset;
const label = 'dynamic-import';
</script>

<div id="dynamic-import">
<span class="label">{label}</span> <img alt="imported" src={importedAsset} />
</div>

<!-- HMR-TEMPLATE-INJECT -->
<style>
.label {
padding-left: 2rem;
background-image: url('/src/assets/dynamic.png');
background-repeat: no-repeat;
background-size: contain;
}
img {
width: 1rem;
height: 1rem;
}
</style>
Loading
Loading