mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
## Summary This PR makes URL serialization logic available to Node.js contexts (like Cypress tests) and replaces a hard-coded 4812-character base64 URL in tests with programmatically generated state. This builds on the shared utilities refactoring from #8246. ### Changes #### 1. Extract URL Serialization to Shared Module **Problem:** URL serialization code depended on GoldenLayout's browser-only ConfigMinifier, preventing Cypress spec files from importing it (they load in Node.js before running in browser). **Solution:** Created `shared/url-serialization.ts` with a Node-compatible ConfigMinifier reimplementation. **Technical Details:** - Reimplemented GoldenLayout's ConfigMinifier without browser dependencies - Moved serialization functions (`serialiseState`, `deserialiseState`, `risonify`, `unrisonify`) to shared module - Moved minification functions (`minifyConfig`, `unminifyConfig`) to shared module - Updated `static/url.ts` to use shared module instead of GoldenLayout - Added comprehensive test coverage in `test/url-serialization.ts` **Files:** - **New:** `shared/url-serialization.ts` (~279 lines) - **Modified:** `static/url.ts` (removed ~30 lines, eliminated GoldenLayout dependency) - **New:** `test/url-serialization.ts` (~96 lines) #### 2. Replace Hard-coded Cypress URL with Programmatic State **Before:** A hard-coded 4812-character base64 URL containing state for all panes ```typescript cy.visit('http://localhost:10240/#z:OYLghAFBqd5TB8IAsQGMD2ATApgUWwEsAXTAJwBoiQIAzIgG...'); ``` **After:** Programmatically generated state using `buildKnownGoodState()` function ```typescript const state = buildKnownGoodState(); const hash = serialiseState(state); cy.visit(`http://localhost:10240/#${hash}`, {...}); ``` **Benefits:** - Human-readable, maintainable test state - Programmatic generation from `PANE_DATA_MAP` keys - Layout optimized with 8 panes per row - Produces identical compressed URL format - Much easier to add/modify panes in the future #### 3. PANE_DATA_MAP Consistency Improvements Updated `PANE_DATA_MAP` to use component names exactly as registered with GoldenLayout: **Key renames:** - `preprocessor` → `pp` - `llvmir` → `ir` - `pipeline` → `llvmOptPipelineView` - `mir` → `rustmir` - `hir` → `rusthir` - `macro` → `rustmacroexp` - `core` → `haskellCore` - `stg` → `haskellStg` - `cmm` → `haskellCmm` - `dump` → `gccdump` - `tree` → `gnatdebugtree` - `debug` → `gnatdebug` **Added panes:** `codeEditor`, `compiler`, `conformance`, `output` (were missing from map) **Re-enabled tests:** - `yul` pane test (was commented out, now fixed) - `clojuremacroexp` pane test (was commented out, now fixed) - `cfg` pane test (had TODO, now removed) **Why this matters:** The `buildKnownGoodState()` function uses `Object.keys(PANE_DATA_MAP)` as the `componentName` property, so keys must match the actual registered component names for GoldenLayout to find them. ## Test Plan - [x] All Cypress tests pass (confirmed by @mattgodbolt) - [x] TypeScript compilation passes (`npm run ts-check`) - [x] Linting passes (`npm run lint`) - [x] URL serialization tests pass (3/3 tests) - [x] Pre-commit hooks pass - [x] Related vitest tests pass ## Dependencies - Builds on #8246 (shared utilities refactoring - already merged) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
202 lines
8.3 KiB
TypeScript
202 lines
8.3 KiB
TypeScript
import {serialiseState} from '../../shared/url-serialization.js';
|
|
import {assertNoConsoleOutput, stubConsoleOutput} from '../support/utils';
|
|
|
|
const PANE_DATA_MAP = {
|
|
codeEditor: {name: 'Editor', selector: 'new-editor'},
|
|
compiler: {name: 'Compiler', selector: 'new-compiler'},
|
|
conformance: {name: 'Conformance', selector: 'new-conformance'},
|
|
output: {name: 'Output', selector: 'new-output-pane'},
|
|
executor: {name: 'Executor', selector: 'create-executor'},
|
|
opt: {name: 'Opt Viewer', selector: 'view-optimization'},
|
|
stackusage: {name: 'Stack Usage Viewer', selector: 'view-stack-usage'},
|
|
pp: {name: 'Preprocessor', selector: 'view-pp'},
|
|
ast: {name: 'Ast Viewer', selector: 'view-ast'},
|
|
ir: {name: 'LLVM IR', selector: 'view-ir'},
|
|
llvmOptPipelineView: {name: 'Pipeline', selector: 'view-opt-pipeline'},
|
|
device: {name: 'Device', selector: 'view-device'},
|
|
rustmir: {name: 'MIR', selector: 'view-rustmir'},
|
|
rusthir: {name: 'HIR', selector: 'view-rusthir'},
|
|
rustmacroexp: {name: 'Macro', selector: 'view-rustmacroexp'},
|
|
haskellCore: {name: 'Core', selector: 'view-haskellCore'},
|
|
haskellStg: {name: 'STG', selector: 'view-haskellStg'},
|
|
haskellCmm: {name: 'Cmm', selector: 'view-haskellCmm'},
|
|
yul: {name: 'Yul', selector: 'view-yul'},
|
|
clojuremacroexp: {name: 'Clojure Macro', selector: 'view-clojuremacroexp'},
|
|
gccdump: {name: 'Tree/RTL', selector: 'view-gccdump'},
|
|
gnatdebugtree: {name: 'Tree', selector: 'view-gnatdebugtree'},
|
|
gnatdebug: {name: 'Debug', selector: 'view-gnatdebug'},
|
|
cfg: {name: 'CFG', selector: 'view-cfg'},
|
|
explain: {name: 'Claude Explain', selector: 'view-explain'},
|
|
};
|
|
|
|
describe('Individual pane testing', () => {
|
|
beforeEach(() => {
|
|
cy.visit('/', {
|
|
onBeforeLoad: win => {
|
|
stubConsoleOutput(win);
|
|
},
|
|
});
|
|
|
|
cy.get('[data-cy="new-compiler-dropdown-btn"]:visible').click();
|
|
// Shows every pane button even if the compiler does not support it
|
|
cy.get('[data-cy="new-compiler-pane-dropdown"]:visible button').each($btn => {
|
|
$btn.prop('disabled', false).show();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Ensure no output in console
|
|
return cy.window().then(_win => {
|
|
assertNoConsoleOutput();
|
|
});
|
|
});
|
|
|
|
function addPaneOpenTest(paneData) {
|
|
it(paneData.name + ' pane', () => {
|
|
cy.get(`[data-cy="new-${paneData.selector}-btn"]:visible`).click();
|
|
// Not the most consistent way, but the easiest one!
|
|
cy.get('span.lm_title:visible').contains(paneData.name);
|
|
});
|
|
}
|
|
|
|
addPaneOpenTest(PANE_DATA_MAP.executor);
|
|
addPaneOpenTest(PANE_DATA_MAP.opt);
|
|
addPaneOpenTest(PANE_DATA_MAP.pp);
|
|
addPaneOpenTest(PANE_DATA_MAP.ast);
|
|
addPaneOpenTest(PANE_DATA_MAP.ir);
|
|
addPaneOpenTest(PANE_DATA_MAP.llvmOptPipelineView);
|
|
addPaneOpenTest(PANE_DATA_MAP.device);
|
|
addPaneOpenTest(PANE_DATA_MAP.rustmir);
|
|
addPaneOpenTest(PANE_DATA_MAP.rusthir);
|
|
addPaneOpenTest(PANE_DATA_MAP.rustmacroexp);
|
|
addPaneOpenTest(PANE_DATA_MAP.haskellCore);
|
|
addPaneOpenTest(PANE_DATA_MAP.haskellStg);
|
|
addPaneOpenTest(PANE_DATA_MAP.haskellCmm);
|
|
addPaneOpenTest(PANE_DATA_MAP.yul);
|
|
addPaneOpenTest(PANE_DATA_MAP.clojuremacroexp);
|
|
addPaneOpenTest(PANE_DATA_MAP.gccdump);
|
|
addPaneOpenTest(PANE_DATA_MAP.gnatdebugtree);
|
|
addPaneOpenTest(PANE_DATA_MAP.gnatdebug);
|
|
addPaneOpenTest(PANE_DATA_MAP.stackusage);
|
|
addPaneOpenTest(PANE_DATA_MAP.explain);
|
|
addPaneOpenTest(PANE_DATA_MAP.cfg);
|
|
|
|
it('Output pane', () => {
|
|
// Hide the dropdown
|
|
cy.get('[data-cy="new-compiler-dropdown-btn"]:visible').click();
|
|
cy.get(`[data-cy="new-output-pane-btn"]:visible`).click();
|
|
cy.get('span.lm_title:visible').contains('Output');
|
|
});
|
|
|
|
it('Conformance view pane', () => {
|
|
// First, hide the dropdown
|
|
cy.get('[data-cy="new-compiler-dropdown-btn"]:visible').click();
|
|
cy.get('[data-cy="new-editor-dropdown-btn"]:visible').click();
|
|
cy.get('[data-cy="new-conformance-btn"]:visible').click();
|
|
cy.get('span.lm_title:visible').contains('Conformance');
|
|
});
|
|
});
|
|
|
|
// Programmatically built state containing all pane types for comprehensive testing.
|
|
// This replaced the hard-coded base64 URL that was 4812 characters long.
|
|
// The state is human-readable and maintainable, then serialized to the same compressed format.
|
|
function buildKnownGoodState() {
|
|
const editorId = 1;
|
|
const compilerId = 1;
|
|
const lang = 'c++';
|
|
const source = '// Type your code here, or load an example.\nint square(int num) {\n return num * num;\n}';
|
|
|
|
// Helper functions to reduce boilerplate
|
|
const pane = (componentName: string, componentState: any) => ({
|
|
type: 'component',
|
|
componentName,
|
|
componentState,
|
|
isClosable: true,
|
|
});
|
|
|
|
const stack = (content: any) => ({
|
|
type: 'stack',
|
|
isClosable: true,
|
|
activeItemIndex: 0,
|
|
content: [content],
|
|
});
|
|
|
|
// Define minimal component states for each pane type
|
|
const paneStates: Record<string, any> = {
|
|
codeEditor: {id: editorId, lang, source},
|
|
compiler: {compiler: 'gdefault', id: compilerId, lang, source: editorId},
|
|
conformance: {editorid: editorId, langId: lang, source},
|
|
output: {compiler: compilerId},
|
|
executor: {compiler: compilerId},
|
|
opt: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
stackusage: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
pp: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
ast: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
ir: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
llvmOptPipelineView: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
device: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
rustmir: {compilerName: 'g++ default', editorid: editorId, id: compilerId, treeid: 0},
|
|
rusthir: {compilerName: 'g++ default', editorid: editorId, id: compilerId, treeid: 0},
|
|
rustmacroexp: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
haskellCore: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
haskellStg: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
haskellCmm: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
yul: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
clojuremacroexp: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
gccdump: {
|
|
compilerName: 'g++ default',
|
|
editorid: editorId,
|
|
id: compilerId,
|
|
treeid: 0,
|
|
gccDumpOptions: {},
|
|
},
|
|
gnatdebugtree: {compilerName: 'g++ default', editorid: editorId, id: compilerId, treeid: 0},
|
|
gnatdebug: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
cfg: {editorid: editorId, id: compilerId},
|
|
explain: {compilerName: 'g++ default', editorid: editorId, id: compilerId},
|
|
};
|
|
|
|
// Build all panes from PANE_DATA_MAP
|
|
const allPanes = Object.keys(PANE_DATA_MAP).map(key => stack(pane(key, paneStates[key])));
|
|
|
|
// Chunk panes into rows of 8 for a more reasonable layout
|
|
const panesPerRow = 8;
|
|
const rows = [];
|
|
for (let i = 0; i < allPanes.length; i += panesPerRow) {
|
|
rows.push({
|
|
type: 'row',
|
|
content: allPanes.slice(i, i + panesPerRow),
|
|
});
|
|
}
|
|
|
|
return {
|
|
version: 4,
|
|
content: [{type: 'column', content: rows}],
|
|
};
|
|
}
|
|
|
|
describe('Known good state test', () => {
|
|
beforeEach(() => {
|
|
const state = buildKnownGoodState();
|
|
const hash = serialiseState(state);
|
|
cy.visit(`http://localhost:10240/#${hash}`, {
|
|
onBeforeLoad: win => {
|
|
stubConsoleOutput(win);
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
return cy.window().then(_win => {
|
|
assertNoConsoleOutput();
|
|
});
|
|
});
|
|
|
|
it('Correctly loads the page for a state with every pane active', () => {
|
|
for (const paneId in PANE_DATA_MAP) {
|
|
const pane = PANE_DATA_MAP[paneId];
|
|
cy.get('span.lm_title:visible').contains(pane.name);
|
|
}
|
|
});
|
|
});
|