mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
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:
@@ -1,9 +0,0 @@
|
||||
import {runFrontendTest} from '../support/utils';
|
||||
|
||||
describe('Motd testing', () => {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
runFrontendTest('motd');
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import {runFrontendTest} from '../support/utils';
|
||||
|
||||
describe('RemoteId testing', () => {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
runFrontendTest('remoteId');
|
||||
});
|
||||
@@ -1,13 +1,5 @@
|
||||
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) {
|
||||
cy.stub(win.console, 'log').as('consoleLog');
|
||||
cy.stub(win.console, 'warn').as('consoleWarn');
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
# Frontend testing
|
||||
|
||||
We have a mixture of typescript in the main website's code (located in `static/tests`) and Cypress (located in
|
||||
`cypress/e2e`) to test and report on the workings of that code.
|
||||
We have a mixture of unit tests (located in `static/tests`) and Cypress UI tests(located in `cypress/e2e`).
|
||||
|
||||
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
|
||||
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)
|
||||
To run the cypress tests, see [this document](../UsingCypress.md).
|
||||
|
||||
1603
package-lock.json
generated
1603
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -111,12 +111,14 @@
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
"cypress": "^14.4.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"mock-fs": "^5.5.0",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"nock": "^14.0.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"sass": "^1.89.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"source-map-loader": "^5.0.0",
|
||||
@@ -156,7 +158,11 @@
|
||||
"update-browserslist": "npx browserslist@latest -- --update-db",
|
||||
"prepare": "husky",
|
||||
"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"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import {Options} from './options.interfaces.js';
|
||||
import {IFrontendTesting} from './tests/frontend-testing.interfaces.js';
|
||||
|
||||
export type CompilerExplorerOptions = Record<string, unknown> & Options;
|
||||
|
||||
@@ -32,7 +31,6 @@ declare global {
|
||||
httpRoot: string;
|
||||
staticRoot: string;
|
||||
compilerExplorerOptions: CompilerExplorerOptions;
|
||||
compilerExplorerFrontendTesting: IFrontendTesting;
|
||||
hasUIBeenReset: boolean;
|
||||
PRODUCTION: boolean;
|
||||
onSponsorClick: (sponsorUrl: string) => void;
|
||||
|
||||
@@ -76,11 +76,6 @@ import changelogDocument from './generated/changelog.pug';
|
||||
import cookiesDocument from './generated/cookies.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
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'golden-layout/src/css/goldenlayout-base.css';
|
||||
|
||||
5
static/tests/README.md
Normal file
5
static/tests/README.md
Normal 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2023, Compiler Explorer Authors
|
||||
// Copyright (c) 2025, Compiler Explorer Authors
|
||||
// All rights reserved.
|
||||
//
|
||||
// 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
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import './frontend-testing';
|
||||
import './hello-world';
|
||||
import './motd';
|
||||
import './remote-id';
|
||||
globalThis.__webpack_public_path__ = '';
|
||||
document.body.innerHTML = '<div id="config" httpRoot="/test/" staticRoot="/test/static/" extraOptions="{}"></div>';
|
||||
682
static/tests/components-tests.ts
Normal file
682
static/tests/components-tests.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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
361
static/tests/motd-tests.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022, Compiler Explorer Authors
|
||||
// Copyright (c) 2025, Compiler Explorer Authors
|
||||
// All rights reserved.
|
||||
//
|
||||
// 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
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import {ITestable} from './frontend-testing.interfaces.js';
|
||||
import {getRemoteId} from '../../shared/remote-utils.js';
|
||||
|
||||
class HelloWorldTests implements ITestable {
|
||||
public readonly description: string = 'HelloWorld';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {UrlTestCases} from '../../shared/url-testcases.js';
|
||||
|
||||
public async run() {
|
||||
const person = true;
|
||||
if (!person) throw new Error('HelloWorldTests failed');
|
||||
}
|
||||
}
|
||||
|
||||
window.compilerExplorerFrontendTesting.add(new HelloWorldTests());
|
||||
describe('Remote ID Tests', () => {
|
||||
it.each(UrlTestCases)('Check $remoteUrl $language', testCase => {
|
||||
expect(getRemoteId(testCase.remoteUrl, testCase.language)).toBe(testCase.expectedId);
|
||||
});
|
||||
});
|
||||
5
tsconfig.frontend.tests.json
Normal file
5
tsconfig.frontend.tests.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.frontend.json",
|
||||
"files": ["static/global.ts"],
|
||||
"include": ["static/tests/**/*.ts"],
|
||||
}
|
||||
@@ -6,8 +6,24 @@ export default defineConfig({
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['test/**/*.ts'],
|
||||
exclude: ['test/_*.ts', 'test/utils.ts'],
|
||||
setupFiles: ['/test/_setup-fake-aws.ts', '/test/_setup-log.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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user