Unit testing the frontend (#7829)

This adds some unit tests for the front end.

- configures "frontend tests" as a unit tests in `static/tests`,
removing the old cypress-requiring "unit" tests
- hack enough of a DOM  to get things working
- port motd and id tests
- *adds* a golden layout checks  (see #7807)
- Updates READMEs etc

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Matt Godbolt
2025-06-19 08:46:57 -05:00
committed by GitHub
parent 02b4aa9f02
commit 23fad10939
20 changed files with 2701 additions and 470 deletions

View File

@@ -1,9 +0,0 @@
import {runFrontendTest} from '../support/utils';
describe('Motd testing', () => {
before(() => {
cy.visit('/');
});
runFrontendTest('motd');
});

View File

@@ -1,9 +0,0 @@
import {runFrontendTest} from '../support/utils';
describe('RemoteId testing', () => {
before(() => {
cy.visit('/');
});
runFrontendTest('remoteId');
});

View File

@@ -1,13 +1,5 @@
import '../../static/global'; import '../../static/global';
export function runFrontendTest(name: string) {
it(name, () => {
return cy.window().then(win => {
return win.compilerExplorerFrontendTesting.run(name);
});
});
}
export function stubConsoleOutput(win: Cypress.AUTWindow) { export function stubConsoleOutput(win: Cypress.AUTWindow) {
cy.stub(win.console, 'log').as('consoleLog'); cy.stub(win.console, 'log').as('consoleLog');
cy.stub(win.console, 'warn').as('consoleWarn'); cy.stub(win.console, 'warn').as('consoleWarn');

View File

@@ -1,34 +1,12 @@
# Frontend testing # Frontend testing
We have a mixture of typescript in the main website's code (located in `static/tests`) and Cypress (located in We have a mixture of unit tests (located in `static/tests`) and Cypress UI tests(located in `cypress/e2e`).
`cypress/e2e`) to test and report on the workings of that code.
But there's always the possibility to use Cypress code to do UI checks and testing. If possible, create unit tests in `static/tests` for the code you are working on. If you can get away with simple state
tests and don't need to do any real DOM manipulation, this is the way to go. Testing "does this filter correctly" or "do
we parse this right" are perfect examples of this. These tests use `happy-dom` for _extremely minimal_ DOM mocking just
enough to get the code running at all.
## Recommended If you need to check actual behaviour or rely on the pug loading/HTML rendering, you should use the Cypress tests.
The recommended way of testing is to use typescript to test the inner workings of the various interfaces that are To run the cypress tests, see [this document](../UsingCypress.md).
available.
This has the advantage of having types and being able to verify your code is consistent with the rest of the website and
probably going to run correctly - without having to startup the website and Cypress.
## Adding a test
Steps to add a test:
- Create a new file in `static/tests` (copy paste from `static/tests/hello-world.ts`)
- Make sure to change the `description` as well as the test
- Add the file to the imports of `static/tests/_all.ts`
- Add a test file with a `runFrontendTest()` call in `cypress/e2e`
## Starting tests locally
You don't need to install an entire X server to actually run cypress (just xfvb).
You can find a complete list at https://docs.cypress.io/guides/getting-started/installing-cypress#System-requirements
If you have the prerequisites installed, you should be able to run `npx cypress run` - however, you will need to start
the CE website separately in another terminal before that.
Some extra tips can be found [here](../UsingCypress.md)

1603
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -111,12 +111,14 @@
"css-minimizer-webpack-plugin": "^7.0.2", "css-minimizer-webpack-plugin": "^7.0.2",
"cypress": "^14.4.1", "cypress": "^14.4.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"happy-dom": "^18.0.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.1.0", "lint-staged": "^16.1.0",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"mock-fs": "^5.5.0", "mock-fs": "^5.5.0",
"monaco-editor-webpack-plugin": "^7.1.0", "monaco-editor-webpack-plugin": "^7.1.0",
"nock": "^14.0.5", "nock": "^14.0.5",
"npm-run-all": "^4.1.5",
"sass": "^1.89.2", "sass": "^1.89.2",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"source-map-loader": "^5.0.0", "source-map-loader": "^5.0.0",
@@ -156,7 +158,11 @@
"update-browserslist": "npx browserslist@latest -- --update-db", "update-browserslist": "npx browserslist@latest -- --update-db",
"prepare": "husky", "prepare": "husky",
"ts-compile": "tsc", "ts-compile": "tsc",
"ts-check": "tsc -p ./tsconfig.backend.json --noEmit && tsc -p ./tsconfig.frontend.json --noEmit && tsc -p ./tsconfig.tests.json --noEmit", "ts-check": "npm-run-all ts-check:*",
"ts-check:backend": "tsc -p ./tsconfig.backend.json --noEmit",
"ts-check:frontend": "tsc -p ./tsconfig.frontend.json --noEmit",
"ts-check:tests": "tsc -p ./tsconfig.tests.json --noEmit",
"ts-check:frontend-tests": "tsc -p ./tsconfig.frontend.tests.json --noEmit",
"webpack": "node --no-warnings=ExperimentalWarning --import=tsx ./node_modules/webpack-cli/bin/cli.js --node-env=production --config webpack.config.esm.ts" "webpack": "node --no-warnings=ExperimentalWarning --import=tsx ./node_modules/webpack-cli/bin/cli.js --node-env=production --config webpack.config.esm.ts"
}, },
"license": "BSD-2-Clause", "license": "BSD-2-Clause",

View File

@@ -23,7 +23,6 @@
// POSSIBILITY OF SUCH DAMAGE. // POSSIBILITY OF SUCH DAMAGE.
import {Options} from './options.interfaces.js'; import {Options} from './options.interfaces.js';
import {IFrontendTesting} from './tests/frontend-testing.interfaces.js';
export type CompilerExplorerOptions = Record<string, unknown> & Options; export type CompilerExplorerOptions = Record<string, unknown> & Options;
@@ -32,7 +31,6 @@ declare global {
httpRoot: string; httpRoot: string;
staticRoot: string; staticRoot: string;
compilerExplorerOptions: CompilerExplorerOptions; compilerExplorerOptions: CompilerExplorerOptions;
compilerExplorerFrontendTesting: IFrontendTesting;
hasUIBeenReset: boolean; hasUIBeenReset: boolean;
PRODUCTION: boolean; PRODUCTION: boolean;
onSponsorClick: (sponsorUrl: string) => void; onSponsorClick: (sponsorUrl: string) => void;

View File

@@ -76,11 +76,6 @@ import changelogDocument from './generated/changelog.pug';
import cookiesDocument from './generated/cookies.pug'; import cookiesDocument from './generated/cookies.pug';
import privacyDocument from './generated/privacy.pug'; import privacyDocument from './generated/privacy.pug';
if (!window.PRODUCTION && !options.embedded) {
// TODO: Replace with top-level await import() when we move to Vite
require('./tests/_all');
}
//css //css
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-base.css';

5
static/tests/README.md Normal file
View File

@@ -0,0 +1,5 @@
## Front-end unit tests
Tests here are _super simple_ and use `jsdom` with a fake document to do quick checks on the front-end code.
For anything requiring a real browser, use the `cypress` tests in the `cypress` directory.

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2023, Compiler Explorer Authors // Copyright (c) 2025, Compiler Explorer Authors
// All rights reserved. // All rights reserved.
// //
// Redistribution and use in source and binary forms, with or without // Redistribution and use in source and binary forms, with or without
@@ -22,7 +22,5 @@
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE. // POSSIBILITY OF SUCH DAMAGE.
import './frontend-testing'; globalThis.__webpack_public_path__ = '';
import './hello-world'; document.body.innerHTML = '<div id="config" httpRoot="/test/" staticRoot="/test/static/" extraOptions="{}"></div>';
import './motd';
import './remote-id';

View File

@@ -0,0 +1,682 @@
// Copyright (c) 2025, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import {describe, expect, it} from 'vitest';
import {
createComponentConfig,
createLayoutItem,
fromGoldenLayoutConfig,
toGoldenLayoutConfig,
} from '../../static/components.js';
import {
COMPILER_COMPONENT_NAME,
DIFF_VIEW_COMPONENT_NAME,
EDITOR_COMPONENT_NAME,
EXECUTOR_COMPONENT_NAME,
OPT_VIEW_COMPONENT_NAME,
OUTPUT_COMPONENT_NAME,
TOOL_COMPONENT_NAME,
TREE_COMPONENT_NAME,
} from '../../static/components.interfaces.js';
describe('Components validation', () => {
describe('fromGoldenLayoutConfig', () => {
describe('Input validation', () => {
it('should throw for null input', () => {
expect(() => fromGoldenLayoutConfig(null as any)).toThrow('Invalid configuration: must be an object');
});
it('should throw for undefined input', () => {
expect(() => fromGoldenLayoutConfig(undefined as any)).toThrow(
'Invalid configuration: must be an object',
);
});
it('should throw for non-object input', () => {
expect(() => fromGoldenLayoutConfig('string' as any)).toThrow(
'Invalid configuration: must be an object',
);
expect(() => fromGoldenLayoutConfig(123 as any)).toThrow('Invalid configuration: must be an object');
});
it('should accept arrays as valid objects', () => {
// Arrays are objects in JavaScript, so they pass the initial validation
// The content validation will handle any structure issues later
expect(() => fromGoldenLayoutConfig([] as any)).not.toThrow();
});
});
describe('Valid configurations', () => {
it('should accept empty configuration', () => {
const config = {};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept configuration with empty content array', () => {
const config = {content: []};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept valid compiler component', () => {
const config = {
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {
source: 1,
lang: 'c++',
},
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept valid editor component', () => {
const config = {
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {
id: 1,
lang: 'c++',
},
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept valid executor component', () => {
const config = {
content: [
{
type: 'component',
componentName: EXECUTOR_COMPONENT_NAME,
componentState: {
source: 1,
lang: 'c++',
compilationPanelShown: true,
compilerOutShown: false,
},
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept valid output component', () => {
const config = {
content: [
{
type: 'component',
componentName: OUTPUT_COMPONENT_NAME,
componentState: {
tree: 1,
compiler: 2,
editor: 3,
},
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept valid tool component', () => {
const config = {
content: [
{
type: 'component',
componentName: TOOL_COMPONENT_NAME,
componentState: {
tree: 1,
toolId: 'readelf',
id: 2,
editorid: 3,
},
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should accept valid layout items', () => {
const config = {
content: [
{
type: 'row',
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {},
},
{
type: 'column',
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {source: 1, lang: 'c++'},
},
],
},
],
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
});
describe('Invalid content structure', () => {
it('should throw for non-array content', () => {
const config = {content: 'not-array'} as any;
expect(() => fromGoldenLayoutConfig(config)).toThrow('Configuration content must be an array');
});
it('should throw for content with non-object items', () => {
const config = {content: ['string-item']} as any;
expect(() => fromGoldenLayoutConfig(config)).toThrow('Invalid item 0: must be an object');
});
it('should throw for items missing type', () => {
const config = {content: [{}]} as any;
expect(() => fromGoldenLayoutConfig(config)).toThrow("Invalid item 0: missing 'type' property");
});
it('should throw for items with unknown type', () => {
const config = {content: [{type: 'unknown'}]} as any;
expect(() => fromGoldenLayoutConfig(config)).toThrow("Invalid item 0: unknown type 'unknown'");
});
});
describe('Invalid component configurations', () => {
it('should throw for component missing componentName', () => {
const config = {
content: [
{
type: 'component',
componentState: {},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: missing 'componentName' property",
);
});
it('should throw for component with non-string componentName', () => {
const config = {
content: [
{
type: 'component',
componentName: 123,
componentState: {},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: 'componentName' must be a string",
);
});
it('should throw for invalid compiler state', () => {
const config = {
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {
// Missing required properties
invalidProp: true,
},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: invalid component state for component 'compiler'",
);
});
it('should throw for invalid executor state missing boolean flags', () => {
const config = {
content: [
{
type: 'component',
componentName: EXECUTOR_COMPONENT_NAME,
componentState: {
source: 1,
lang: 'c++',
// Missing compilationPanelShown and compilerOutShown
},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: invalid component state for component 'executor'",
);
});
it('should throw for invalid output state missing numeric properties', () => {
const config = {
content: [
{
type: 'component',
componentName: OUTPUT_COMPONENT_NAME,
componentState: {
tree: 'not-number',
compiler: 1,
editor: 1,
},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: invalid component state for component 'output'",
);
});
it('should throw for invalid tool state', () => {
const config = {
content: [
{
type: 'component',
componentName: TOOL_COMPONENT_NAME,
componentState: {
tree: 1,
// Missing required toolId, id, editorid
},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: invalid component state for component 'tool'",
);
});
it('should throw for unknown component name', () => {
const config = {
content: [
{
type: 'component',
componentName: 'unknown-component',
componentState: {},
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: invalid component state for component 'unknown-component'",
);
});
});
describe('Invalid layout item configurations', () => {
it('should throw for layout item missing content', () => {
const config = {
content: [
{
type: 'row',
},
],
} as any;
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: layout items must have a 'content' array",
);
});
it('should throw for layout item with non-array content', () => {
const config = {
content: [
{
type: 'column',
content: 'not-array',
},
],
} as any;
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: layout items must have a 'content' array",
);
});
});
describe('Complex nested validation', () => {
it('should validate deeply nested structures', () => {
const config = {
content: [
{
type: 'row',
content: [
{
type: 'column',
content: [
{
type: 'stack',
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {},
},
{
type: 'component',
componentName: DIFF_VIEW_COMPONENT_NAME,
componentState: {},
},
],
},
],
},
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {source: 1, lang: 'c++'},
},
],
},
],
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should throw for invalid nested component', () => {
const config = {
content: [
{
type: 'row',
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {},
},
{
type: 'column',
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {
// Invalid state - missing required properties
invalidProp: true,
},
},
],
},
],
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow('invalid component state for component');
});
});
describe('Edge cases', () => {
it('should handle null component state', () => {
const config = {
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: null,
},
],
};
expect(() => fromGoldenLayoutConfig(config)).toThrow(
"Invalid item 0: invalid component state for component 'compiler'",
);
});
it('should preserve additional properties', () => {
const config = {
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {},
title: 'Custom Title',
isClosable: false,
width: 50,
},
],
settings: {
showPopoutIcon: false,
},
maximisedItemId: null,
};
const result = fromGoldenLayoutConfig(config);
expect(result).toEqual(config);
});
it('should handle mixed valid and invalid compiler states', () => {
// Test different valid combinations for compiler
const validConfigs = [
{source: 1, lang: 'c++'},
{source: 1, compiler: 'gcc'},
{tree: 1, lang: 'c++'},
];
for (const state of validConfigs) {
const config = {
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: state,
},
],
};
expect(() => fromGoldenLayoutConfig(config)).not.toThrow();
}
});
});
});
describe('Helper functions', () => {
describe('createComponentConfig', () => {
it('should create valid component config', () => {
const config = createComponentConfig(EDITOR_COMPONENT_NAME, {id: 1, lang: 'c++'});
expect(config).toEqual({
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {id: 1, lang: 'c++'},
});
});
it('should include optional properties', () => {
const config = createComponentConfig(
COMPILER_COMPONENT_NAME,
{source: 1, lang: 'c++'},
{
title: 'Custom Compiler',
isClosable: false,
width: 50,
height: 200,
},
);
expect(config).toEqual({
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {source: 1, lang: 'c++'},
title: 'Custom Compiler',
isClosable: false,
width: 50,
height: 200,
});
});
});
describe('createLayoutItem', () => {
it('should create valid layout item', () => {
const content = [
createComponentConfig(EDITOR_COMPONENT_NAME, {}),
createComponentConfig(COMPILER_COMPONENT_NAME, {source: 1, lang: 'c++'}),
];
const layoutItem = createLayoutItem('row', content);
expect(layoutItem).toEqual({
type: 'row',
content,
});
});
it('should include optional properties', () => {
const content = [createComponentConfig(DIFF_VIEW_COMPONENT_NAME, {})];
const layoutItem = createLayoutItem('stack', content, {
isClosable: true,
width: 100,
activeItemIndex: 0,
});
expect(layoutItem).toEqual({
type: 'stack',
content,
isClosable: true,
width: 100,
activeItemIndex: 0,
});
});
});
describe('toGoldenLayoutConfig', () => {
it('should pass through config unchanged', () => {
const config = {
content: [
createLayoutItem('row', [
createComponentConfig(EDITOR_COMPONENT_NAME, {id: 1}),
createComponentConfig(COMPILER_COMPONENT_NAME, {source: 1, lang: 'c++'}),
]),
],
settings: {showPopoutIcon: false},
};
const result = toGoldenLayoutConfig(config);
expect(result).toBe(config);
});
});
});
describe('Integration tests', () => {
it('should round-trip through validation', () => {
const originalConfig = {
content: [
{
type: 'row',
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {id: 1, lang: 'c++'},
},
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {source: 1, lang: 'c++'},
},
],
},
],
settings: {showPopoutIcon: false},
};
const validated = fromGoldenLayoutConfig(originalConfig);
const backToGolden = toGoldenLayoutConfig(validated);
expect(backToGolden).toEqual(originalConfig);
});
it('should work with complex real-world config', () => {
const complexConfig = {
content: [
{
type: 'row',
content: [
{
type: 'column',
width: 50,
content: [
{
type: 'component',
componentName: EDITOR_COMPONENT_NAME,
componentState: {id: 1, lang: 'c++', source: '#include <iostream>'},
height: 60,
},
{
type: 'component',
componentName: TREE_COMPONENT_NAME,
componentState: {id: 1, cmakeArgs: '-DCMAKE_BUILD_TYPE=Release'},
height: 40,
},
],
},
{
type: 'stack',
width: 50,
content: [
{
type: 'component',
componentName: COMPILER_COMPONENT_NAME,
componentState: {
source: 1,
lang: 'c++',
compiler: 'gcc',
options: '-O2',
},
title: 'GCC',
},
{
type: 'component',
componentName: OPT_VIEW_COMPONENT_NAME,
componentState: {id: 1, source: 'test'},
title: 'Optimization',
},
],
},
],
},
],
settings: {
showPopoutIcon: false,
showMaximiseIcon: true,
showCloseIcon: true,
},
maximisedItemId: null,
};
expect(() => fromGoldenLayoutConfig(complexConfig)).not.toThrow();
const result = fromGoldenLayoutConfig(complexConfig);
expect(result).toEqual(complexConfig);
});
});
});

View File

@@ -1,34 +0,0 @@
// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
export interface ITestable {
readonly description: string;
run(): Promise<void>;
}
export interface IFrontendTesting {
add(test: ITestable): void;
getAllTestNames(): string[];
run(testToRun: string): Promise<void>;
}

View File

@@ -1,54 +0,0 @@
// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import {IFrontendTesting, ITestable} from './frontend-testing.interfaces.js';
class FrontendTesting implements IFrontendTesting {
private testSuites: Array<ITestable> = [];
public add(test: ITestable) {
this.testSuites.push(test);
}
public getAllTestNames(): string[] {
return this.testSuites.map(val => val.description);
}
private findTest(name: string) {
for (const suite of this.testSuites) {
if (suite.description === name) {
return suite;
}
}
throw new Error(`Can't find test ${name}`);
}
public async run(testToRun: string) {
const testSuite = this.findTest(testToRun);
await testSuite.run();
}
}
window.compilerExplorerFrontendTesting = new FrontendTesting();

361
static/tests/motd-tests.ts Normal file
View File

@@ -0,0 +1,361 @@
// Copyright (c) 2025, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {isValidAd} from '../../static/motd.js';
describe('MOTD Tests', () => {
describe('with fake timers', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should keep ad if now > from', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(isValidAd({filter: [], html: '', valid_from: '2022-01-01T00:00:00+00:00'}, 'langForTest')).toBe(
true,
);
});
it('should filter ad if now < from', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(isValidAd({filter: [], html: '', valid_from: '2022-01-16T00:00:00+00:00'}, 'langForTest')).toBe(
false,
);
});
it('should keep ad if now < until', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(isValidAd({filter: [], html: '', valid_until: '2022-01-16T00:00:00+00:00'}, 'langForTest')).toBe(
true,
);
});
it('should filter ad if now > until', () => {
vi.setSystemTime(new Date('2022-01-20T00:00:00+00:00'));
expect(isValidAd({filter: [], html: '', valid_until: '2022-01-16T00:00:00+00:00'}, 'langForTest')).toBe(
false,
);
});
it('should keep ad if from < now < until', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(true);
});
it('should filter ad if now < from < until', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: '2022-01-10T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(false);
});
it('should filter ad if from < until < now', () => {
vi.setSystemTime(new Date('2022-01-20T00:00:00+00:00'));
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: '2022-01-10T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(false);
});
it('should filter ad if until < now < from', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: '2022-01-16T00:00:00+00:00',
valid_until: '2022-01-10T00:00:00+00:00',
},
'langForTest',
),
).toBe(false);
});
it('should filter ad if from < now < until but filtered by lang', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: ['fakeLang'],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(false);
});
it('should keep ad if from < now < until and not filtered by lang', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: ['langForTest'],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad if from = now < until and not filtered by lang', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: ['langForTest'],
html: '',
valid_from: '2022-01-08T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad if from < now = until and not filtered by lang', () => {
vi.setSystemTime(new Date('2022-01-08T00:00:00+00:00'));
expect(
isValidAd(
{
filter: ['langForTest'],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-18T00:00:00+00:00',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad when now equals valid_until', () => {
vi.setSystemTime(new Date('2022-01-16T00:00:00+00:00'));
expect(
isValidAd(
{
filter: [],
html: '',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad when now equals valid_from', () => {
vi.setSystemTime(new Date('2022-01-16T00:00:00+00:00'));
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: '2022-01-16T00:00:00+00:00',
},
'langForTest',
),
).toBe(true);
});
});
it('should keep ad if sublang is not set', () => {
expect(isValidAd({filter: [], html: ''}, 'fakeLang')).toBe(true);
});
it('should keep ad if sublang is not set even if filtering for lang', () => {
expect(isValidAd({filter: ['fakeLang'], html: ''}, 'langForTest')).toBe(false);
});
it('should keep ad if no lang is set', () => {
expect(isValidAd({filter: [], html: ''}, 'langForTest')).toBe(true);
});
it('should filter ad if not the correct language', () => {
expect(isValidAd({filter: ['anotherLang'], html: ''}, 'langForTest')).toBe(false);
});
it('should keep ad if the correct language is used', () => {
expect(isValidAd({filter: ['langForTest'], html: ''}, 'langForTest')).toBe(true);
});
it('should keep ad if valid_from has invalid date format', () => {
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: 'invalid-date-format',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad if valid_until has invalid date format', () => {
expect(
isValidAd(
{
filter: [],
html: '',
valid_until: 'not-a-date',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad if both dates have invalid format', () => {
expect(
isValidAd(
{
filter: [],
html: '',
valid_from: 'invalid-from',
valid_until: 'invalid-until',
},
'langForTest',
),
).toBe(true);
});
it('should keep ad with empty string sublang', () => {
expect(isValidAd({filter: [], html: ''}, '')).toBe(true);
});
it('should keep ad with null sublang', () => {
expect(isValidAd({filter: [], html: ''}, null as any)).toBe(true);
});
it('should keep ad with undefined sublang', () => {
expect(isValidAd({filter: [], html: ''}, undefined as any)).toBe(true);
});
it('should keep ad if sublang matches one of multiple filter languages', () => {
expect(
isValidAd(
{
filter: ['lang1', 'lang2', 'langForTest', 'lang3'],
html: '',
},
'langForTest',
),
).toBe(true);
});
it('should filter ad if sublang does not match any of multiple filter languages', () => {
expect(
isValidAd(
{
filter: ['lang1', 'lang2', 'lang3'],
html: '',
},
'langForTest',
),
).toBe(false);
});
it('should filter ad with invalid date and non-matching language filter', () => {
expect(
isValidAd(
{
filter: ['anotherLang'],
html: '',
valid_from: 'invalid-date',
},
'langForTest',
),
).toBe(false);
});
it('should keep ad with invalid date and matching language filter', () => {
expect(
isValidAd(
{
filter: ['langForTest'],
html: '',
valid_until: 'invalid-date',
},
'langForTest',
),
).toBe(true);
});
it('should handle filter with empty strings', () => {
expect(
isValidAd(
{
filter: ['', 'langForTest', ''],
html: '',
},
'langForTest',
),
).toBe(true);
});
it('should handle filter with duplicate languages', () => {
expect(
isValidAd(
{
filter: ['langForTest', 'langForTest', 'langForTest'],
html: '',
},
'langForTest',
),
).toBe(true);
});
});

View File

@@ -1,253 +0,0 @@
// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
/// <reference path="../../node_modules/cypress/types/cypress-global-vars.d.ts" />
import {isValidAd} from '../motd.js';
import {ITestable} from './frontend-testing.interfaces.js';
import {Ad} from '../motd.interfaces.js';
class MotdTests implements ITestable {
public readonly description: string = 'motd';
private static assertAd(ad: Ad, subLang: string, expected: boolean, message: string) {
if (isValidAd(ad, subLang) !== expected) {
throw new Error(message);
}
}
private static assertAdWithDateNow(dateNow: number, ad: Ad, subLang: string, expected: boolean, message: string) {
const originalDateNow = Date.now;
Date.now = () => dateNow;
MotdTests.assertAd(ad, subLang, expected, message);
Date.now = originalDateNow;
}
public async run() {
MotdTests.assertAd(
{
filter: [],
html: '',
},
'fakeLang',
true,
'Keep ad if sublang is not set',
);
MotdTests.assertAd(
{
filter: ['fakeLang'],
html: '',
},
'langForTest',
false,
'Keep ad if sublang is not set even if filtering for lang',
);
MotdTests.assertAd(
{
filter: [],
html: '',
},
'langForTest',
true,
'Keep ad if no lang is set',
);
MotdTests.assertAd(
{
filter: ['anotherLang'],
html: '',
},
'langForTest',
false,
'Filters ad if not the correct language',
);
MotdTests.assertAd(
{
filter: ['langForTest'],
html: '',
},
'langForTest',
true,
'Keep ad if the correct language is used',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
},
'langForTest',
true,
'Keep ad if now > from',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-16T00:00:00+00:00',
},
'langForTest',
false,
'Filter ad if now < from',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
true,
'Keep ad if now < until',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-16T00:00:00+00:00',
},
'langForTest',
false,
'Filter ad if now > until',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
true,
'Keep ad if from < now < until',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-10T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
false,
'Filter ad if now < from < until',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-20T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-10T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
false,
'Filter ad if from < until < now',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: [],
html: '',
valid_from: '2022-01-16T00:00:00+00:00',
valid_until: '2022-01-10T00:00:00+00:00',
},
'langForTest',
false,
'Filter ad if until < now < from',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: ['fakeLang'],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
false,
'Filter ad if from < now < until but filtered by lang',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: ['langForTest'],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
true,
'Keep ad if from < now < until and not filtered by lang',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: ['langForTest'],
html: '',
valid_from: '2022-01-08T00:00:00+00:00',
valid_until: '2022-01-16T00:00:00+00:00',
},
'langForTest',
true,
'Keep ad if from = now < until and not filtered by lang',
);
MotdTests.assertAdWithDateNow(
Date.parse('2022-01-08T00:00:00+00:00'),
{
filter: ['langForTest'],
html: '',
valid_from: '2022-01-01T00:00:00+00:00',
valid_until: '2022-01-18T00:00:00+00:00',
},
'langForTest',
true,
'Keep ad if from < now = until and not filtered by lang',
);
}
}
window.compilerExplorerFrontendTesting.add(new MotdTests());

View File

@@ -1,47 +0,0 @@
// Copyright (c) 2025, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
/// <reference path="../../node_modules/cypress/types/cypress-global-vars.d.ts" />
import {getRemoteId} from '../../shared/remote-utils.js';
import {UrlTestCases} from '../../shared/url-testcases.js';
import {ITestable} from './frontend-testing.interfaces.js';
class RemoteIdTests implements ITestable {
public readonly description: string = 'remoteId';
public async run() {
UrlTestCases.forEach(testCase => {
if (getRemoteId(testCase.remoteUrl, testCase.language) !== testCase.expectedId) {
throw new Error(
`Test case failed for language: ${testCase.language}, remoteUrl: ${testCase.remoteUrl}, expectedId: ${testCase.expectedId}`,
);
}
});
}
}
window.compilerExplorerFrontendTesting.add(new RemoteIdTests());

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2022, Compiler Explorer Authors // Copyright (c) 2025, Compiler Explorer Authors
// All rights reserved. // All rights reserved.
// //
// Redistribution and use in source and binary forms, with or without // Redistribution and use in source and binary forms, with or without
@@ -22,15 +22,13 @@
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE. // POSSIBILITY OF SUCH DAMAGE.
import {ITestable} from './frontend-testing.interfaces.js'; import {getRemoteId} from '../../shared/remote-utils.js';
class HelloWorldTests implements ITestable { import {describe, expect, it} from 'vitest';
public readonly description: string = 'HelloWorld'; import {UrlTestCases} from '../../shared/url-testcases.js';
public async run() { describe('Remote ID Tests', () => {
const person = true; it.each(UrlTestCases)('Check $remoteUrl $language', testCase => {
if (!person) throw new Error('HelloWorldTests failed'); expect(getRemoteId(testCase.remoteUrl, testCase.language)).toBe(testCase.expectedId);
} });
} });
window.compilerExplorerFrontendTesting.add(new HelloWorldTests());

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.frontend.json",
"files": ["static/global.ts"],
"include": ["static/tests/**/*.ts"],
}

View File

@@ -6,8 +6,24 @@ export default defineConfig({
provider: 'v8', provider: 'v8',
reporter: ['text', 'json', 'html'], reporter: ['text', 'json', 'html'],
}, },
include: ['test/**/*.ts'], projects: [
exclude: ['test/_*.ts', 'test/utils.ts'], {
setupFiles: ['/test/_setup-fake-aws.ts', '/test/_setup-log.ts'], test: {
name: 'unit',
include: ['test/**/*.ts'],
exclude: ['test/_*.ts', 'test/utils.ts'],
setupFiles: ['test/_setup-fake-aws.ts', 'test/_setup-log.ts'],
},
},
{
test: {
name: 'frontend unit',
include: ['static/tests/**/*.ts'],
exclude: ['static/tests/_*.ts'],
setupFiles: ['static/tests/_setup-dom.ts'],
environment: 'happy-dom',
},
},
],
}, },
}); });