Files
compiler-explorer/cypress/e2e/frontend.cy.ts
Matt Godbolt 65e4f302b7 URL serialization refactoring and Cypress test improvements (#8215)
## 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>
2025-11-04 14:09:01 -06:00

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);
}
});
});