mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
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>
This commit is contained in:
@@ -45,6 +45,7 @@ let jsCookie = JsCookie;
|
||||
|
||||
import {unwrap} from '../shared/assert.js';
|
||||
import * as utils from '../shared/common-utils.js';
|
||||
import {unrisonify} from '../shared/url-serialization.js';
|
||||
import {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
|
||||
import {LanguageKey} from '../types/languages.interfaces.js';
|
||||
import * as BootstrapUtils from './bootstrap-utils.js';
|
||||
@@ -66,7 +67,7 @@ import {setupRealDark, takeUsersOutOfRealDark} from './real-dark.js';
|
||||
import {Settings, SiteSettings} from './settings.js';
|
||||
import {initialiseSharing} from './sharing.js';
|
||||
import {Themer} from './themes.js';
|
||||
import * as url from './url.js';
|
||||
import {deserialiseState} from './url.js';
|
||||
import {formatISODate, updateAndCalcTopBarHeight} from './utils.js';
|
||||
import {Alert} from './widgets/alert.js';
|
||||
import {HistoryWidget} from './widgets/history-widget.js';
|
||||
@@ -256,7 +257,7 @@ function configFromEmbedded(embeddedUrl: string, defaultLangId: string) {
|
||||
// Old-style link?
|
||||
let params;
|
||||
try {
|
||||
params = url.unrisonify(embeddedUrl);
|
||||
params = unrisonify(embeddedUrl);
|
||||
} catch {
|
||||
document.write(
|
||||
'<div style="padding: 10px; background: #fa564e; color: black;">' +
|
||||
@@ -285,7 +286,7 @@ function configFromEmbedded(embeddedUrl: string, defaultLangId: string) {
|
||||
],
|
||||
};
|
||||
}
|
||||
return url.deserialiseState(embeddedUrl);
|
||||
return deserialiseState(embeddedUrl);
|
||||
}
|
||||
|
||||
function fixBugsInConfig(config: Partial<GoldenLayout.Config & {activeItemIndex?: number}>): void {
|
||||
@@ -332,7 +333,7 @@ function findConfig(
|
||||
config = options.config;
|
||||
} else {
|
||||
try {
|
||||
config = url.deserialiseState(window.location.hash.substring(1));
|
||||
config = deserialiseState(window.location.hash.substring(1));
|
||||
} catch {
|
||||
// #3518 Alert the user that the url is invalid
|
||||
const alertSystem = new Alert();
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import GoldenLayout from 'golden-layout';
|
||||
import {parse} from '../shared/stacktrace.js';
|
||||
import {serialiseState} from '../shared/url-serialization.js';
|
||||
import {options} from './options.js';
|
||||
import {SiteSettings} from './settings.js';
|
||||
import {serialiseState} from './url.js';
|
||||
|
||||
let layout: GoldenLayout;
|
||||
let allowSendCode: boolean;
|
||||
|
||||
@@ -28,12 +28,12 @@ import GoldenLayout from 'golden-layout';
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import {unwrap} from '../shared/assert.js';
|
||||
import {serialiseState} from '../shared/url-serialization.js';
|
||||
import * as BootstrapUtils from './bootstrap-utils.js';
|
||||
import {sessionThenLocalStorage} from './local.js';
|
||||
import {options} from './options.js';
|
||||
import {SentryCapture} from './sentry.js';
|
||||
import {Settings, SiteSettings} from './settings.js';
|
||||
import * as url from './url.js';
|
||||
|
||||
import ClickEvent = JQuery.ClickEvent;
|
||||
|
||||
@@ -106,7 +106,7 @@ export class SharingBase {
|
||||
// Update embedded links if present (works in both modes)
|
||||
if (options.embedded) {
|
||||
const strippedToLast = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
|
||||
$('a.link').prop('href', strippedToLast + '#' + url.serialiseState(config));
|
||||
$('a.link').prop('href', strippedToLast + '#' + serialiseState(config));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ export class Sharing extends SharingBase {
|
||||
Sharing.getShortLink(config, root, done);
|
||||
return;
|
||||
case LinkType.Full:
|
||||
done(null, window.location.origin + root + '#' + url.serialiseState(config), false);
|
||||
done(null, window.location.origin + root + '#' + serialiseState(config), false);
|
||||
return;
|
||||
case LinkType.Embed: {
|
||||
const options: Record<string, boolean> = {};
|
||||
@@ -436,7 +436,7 @@ export class Sharing extends SharingBase {
|
||||
private static getShortLink(config: any, root: string, done: CallableFunction): void {
|
||||
const useExternalShortener = options.urlShortenService !== 'default';
|
||||
const data = JSON.stringify({
|
||||
config: useExternalShortener ? url.serialiseState(config) : config,
|
||||
config: useExternalShortener ? serialiseState(config) : config,
|
||||
});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
@@ -485,7 +485,7 @@ export class Sharing extends SharingBase {
|
||||
|
||||
const path = (readOnly ? 'embed-ro' : 'e') + parameters + '#';
|
||||
|
||||
return location + path + url.serialiseState(config);
|
||||
return location + path + serialiseState(config);
|
||||
}
|
||||
|
||||
private static storeCurrentConfig(config: any, extra: string): void {
|
||||
|
||||
224
static/tests/url-tests.ts
Normal file
224
static/tests/url-tests.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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 {deserialiseState} from '../url.js';
|
||||
|
||||
describe('Historical URL Backward Compatibility', () => {
|
||||
describe('Version 4 (modern minified format)', () => {
|
||||
const findComponent = (content: any[], name: string): any => {
|
||||
for (const item of content) {
|
||||
if (item.componentName === name) return item;
|
||||
if (item.content && Array.isArray(item.content)) {
|
||||
const found = findComponent(item.content, name);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
it('should deserialize uncompressed minified URL', () => {
|
||||
const urlHash =
|
||||
'g:!((g:!((g:!((h:codeEditor,i:(filename:%271%27,fontScale:14,fontUsePx:%270%27,j:1,lang:c%2B%2B,source:%27//+Type+your+code+here,+or+load+an+example.%0Aint+square(int+num)+%7B%0A++++return+num+*+num%3B%0A%7D%27),l:%275%27,n:%270%27,o:%27C%2B%2B+source+%231%27,t:%270%27)),k:50,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:g152,filters:(b:%270%27,binary:%271%27,binaryObject:%271%27,commentOnly:%270%27,debugCalls:%271%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,libraryCode:%270%27,trim:%271%27,verboseDemangling:%270%27),flagsViewOpen:%271%27,fontScale:14,fontUsePx:%270%27,j:1,lang:c%2B%2B,libs:!(),options:%27%27,overrides:!(),selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1),l:%275%27,n:%270%27,o:%27+x86-64+gcc+15.2+(Editor+%231)%27,t:%270%27)),k:50,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27)),l:%272%27,n:%270%27,o:%27%27,t:%270%27)),version:4';
|
||||
|
||||
const state = deserialiseState(urlHash);
|
||||
|
||||
expect(state).toBeTruthy();
|
||||
expect(state.version).toBe(4);
|
||||
expect(state.content).toBeTruthy();
|
||||
expect(Array.isArray(state.content)).toBe(true);
|
||||
|
||||
const editor = findComponent(state.content, 'codeEditor');
|
||||
expect(editor).toBeTruthy();
|
||||
expect(editor.componentState.source).toContain('square');
|
||||
|
||||
const compiler = findComponent(state.content, 'compiler');
|
||||
expect(compiler).toBeTruthy();
|
||||
expect(compiler.componentState.compiler).toBe('g152');
|
||||
});
|
||||
|
||||
it('should deserialize compressed minified URL', () => {
|
||||
const urlHash =
|
||||
'z:OYLghAFBqd5TKALEBjA9gEwKYFFMCWALugE4A0BIEAZgQDbYB2AhgLbYgDkAjF%2BTXRMiAZVQtGIHgBYBQogFUAztgAKAD24AGfgCsp5eiyahUAUgBMAIUtXyKxqiIEh1ZpgDC6egFc2TEAtZdwAZAiZsADk/ACNsUgMAB3QlYhcmL19/QNlk1OchMIjotjiEnntsRwKmESIWUiJMvwCgyur0uoaiIqjY%2BIMlesbm7Lah7t6SsqkASnt0H1JUTi4AejWAahiWTGB47d390kO9%2BK55%2Bm4AVn4Arh1ydG4PW1tNpUXl7E3LAGY%2BOQiNoLvMANYga5aQzcaT8NiQ6H3R7PLj8JQgaHAh4XchwWAoCaoME%2BJQsfaUagYNiJBjxSLsVbqAAcADYALSs6SbYCoVCbHjXAB0Fn42EIJFIBEwBkEwjEEk4MjkwmUak0OPI%2BgqDmwTnSbiYnm8LQMoXCfVKAwqeTSQlGARtKTtTCm/XK7T1NS6IxN2R1VS9nWGPQt02t9hDDsGIbdVvK8yIpGw2GlmMuNzuIKe3C6xM2ynJPwAagRsAB3A4sjlcnl8gXCiybCD4YhkX4WAHkTZeGl0k7/HizfjYnSzeZIbC7AYQDNcOHkBFQ8jI/io9GYoEg8fkCHLq5cP5ZzXrrc4%2Bb4hDwCAoam0xgUKgQO/9kDAQUWAQMIjxDEQGLZjE4QNAAntwgJAawpAgQA8jEuheuB/DUhwwgwUw9BgZqOAxD4wAeBI9AYrw/A4GwxjAJI2EEMm%2BoAG7YMRjzYOoeo%2BD%2BSGUMIVTZvQBAxKQoFeDg2ZJgQCIkeQDGkDEKTYAAItg5EmHxJjbgIRjAEopYVjBiTMJxcqiOIkjKkZaoaNm2qGBRaBvNYhj8RikDzOgiQ1MR7IwaKTzSVKODObOnr6q4EDuNG0isuQ5rFO6STOjUEVRbaNRxjMAYdEIPpNH6ASRcF3qxmGcU6lGuUgPlEyNGl1rzJ8SwrHMMJcLcK7ZqimzVpy3K8vygois2raSh2ALDmeY7goizULkuSLtdwG5Yupl43iAiSJJSz5GCY7KCKQ5HYoC4ptv5srfvE7KTtOpBMfwRkKqZsjmSolmatZSYpmmWhzq1q45lwqjJokpDoCsSifCcMHsYk7GdWy3V1n1jaDRK7aDt2vb3gcg5jaOoLkFdOAJEFB4zVNf2nhiS3npN%2B7cEebUngt434wePkU8zeM7tJqSuNIQA%3D%3D';
|
||||
|
||||
const state = deserialiseState(urlHash);
|
||||
|
||||
expect(state).toBeTruthy();
|
||||
expect(state.version).toBe(4);
|
||||
expect(state.content).toBeTruthy();
|
||||
expect(Array.isArray(state.content)).toBe(true);
|
||||
|
||||
const editor = findComponent(state.content, 'codeEditor');
|
||||
expect(editor).toBeTruthy();
|
||||
expect(editor.componentState.source).toContain('badger');
|
||||
|
||||
const compiler = findComponent(state.content, 'compiler');
|
||||
expect(compiler).toBeTruthy();
|
||||
expect(compiler.componentState.compiler).toBe('g152');
|
||||
|
||||
const pp = findComponent(state.content, 'pp');
|
||||
expect(pp).toBeTruthy();
|
||||
|
||||
const stackUsage = findComponent(state.content, 'stackusage');
|
||||
expect(stackUsage).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version 2 (JSON format from 2013)', () => {
|
||||
it('should deserialize ICC example with filters', () => {
|
||||
// From etc/oldhash.txt
|
||||
const urlHash =
|
||||
'%7B%22version%22%3A2%2C%22source%22%3A%22%23include%20%3Cxmmintrin.h%3E%5Cn%5Cnvoid%20f(__m128%20a%2C%20__m128%20b)%5Cn%7B%5Cn%20%20%2F%2F%20I%20am%20a%20walrus.%5Cn%7D%22%2C%22compiler%22%3A%22%2Fhome%2Fmgodbolt%2Fapps%2Fintel-icc-oss%2Fbin%2Ficc%22%2C%22options%22%3A%22-O3%20-std%3Dc%2B%2B0x%22%2C%22filterAsm%22%3A%7B%22labels%22%3Atrue%2C%22directives%22%3Atrue%7D%7D';
|
||||
|
||||
const state = deserialiseState(urlHash);
|
||||
|
||||
expect(state).toBeTruthy();
|
||||
expect(state.version).toBe(4);
|
||||
expect(state.content).toBeTruthy();
|
||||
expect(Array.isArray(state.content)).toBe(true);
|
||||
expect(state.content.length).toBeGreaterThan(0);
|
||||
|
||||
const row = state.content[0];
|
||||
expect(row.type).toBe('row');
|
||||
expect(Array.isArray(row.content)).toBe(true);
|
||||
expect(row.content.length).toBe(2);
|
||||
|
||||
const editor = row.content.find((c: any) => c.componentName === 'codeEditor');
|
||||
expect(editor).toBeTruthy();
|
||||
expect(editor.componentState.source).toContain('#include <xmmintrin.h>');
|
||||
expect(editor.componentState.source).toContain('I am a walrus');
|
||||
|
||||
const compiler = row.content.find((c: any) => c.componentName === 'compiler');
|
||||
expect(compiler).toBeTruthy();
|
||||
expect(compiler.componentState.compiler).toBe('/home/mgodbolt/apps/intel-icc-oss/bin/icc');
|
||||
expect(compiler.componentState.options).toBe('-O3 -std=c++0x');
|
||||
|
||||
const filters = compiler.componentState.filters;
|
||||
expect(filters.labels).toBe(true);
|
||||
expect(filters.directives).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version 3 (Rison format)', () => {
|
||||
it('should deserialize GCC 7 widgets example', () => {
|
||||
// From etc/oldhash.txt
|
||||
const urlHash =
|
||||
"compilers:!((compiler:g7snapshot,options:'-std%3Dc%2B%2B1z+-O3+',source:'%23include+%3Cvector%3E%0A%0Astruct+Widget+%7B%0A++int+n%3B%0A++double+x,+y%3B%0A++Widget(const+Widget%26+o)+:+x(o.x),+y(o.y),+n(o.n)+%7B%7D%0A++Widget(int+n,+double+x,+double+y)+:+n(n),+x(x),+y(y)+%7B%7D%0A%7D%3B%0A%0Astd::vector%3CWidget%3E+vector%3B%0Aconst+int+N+%3D+1002%3B%0Adouble+a+%3D+0.1%3B%0Adouble+b+%3D+0.2%3B%0A%0Avoid+demo()+%7B%0A++vector.reserve(N)%3B%0A++for+(int+i+%3D+01%3B+i+%3C+N%3B+%2B%2Bi)%0A++%7B%0A%09Widget+w+%7Bi,+a,+b%7D%3B%0A%09vector.push_back(w)%3B+//+or+vector.push_back(std::move(w))%0A++%7D%0A%7D%0A%0Aint+main()%0A%7B%0A+%0A+%0A%7D%0A')),filterAsm:(colouriseAsm:!t,commentOnly:!t,directives:!t,intel:!t,labels:!t),version:3";
|
||||
|
||||
const state = deserialiseState(urlHash);
|
||||
|
||||
expect(state).toBeTruthy();
|
||||
expect(state.version).toBe(4);
|
||||
expect(state.content).toBeTruthy();
|
||||
expect(Array.isArray(state.content)).toBe(true);
|
||||
|
||||
const row = state.content[0];
|
||||
expect(row.type).toBe('row');
|
||||
expect(Array.isArray(row.content)).toBe(true);
|
||||
expect(row.content.length).toBe(2);
|
||||
|
||||
const editor = row.content.find((c: any) => c.componentName === 'codeEditor');
|
||||
expect(editor).toBeTruthy();
|
||||
expect(editor.componentState.source).toContain('#include <vector>');
|
||||
expect(editor.componentState.source).toContain('struct Widget');
|
||||
expect(editor.componentState.source).toContain('std::vector<Widget> vector');
|
||||
expect(editor.componentState.options.colouriseAsm).toBe(true);
|
||||
|
||||
const compiler = row.content.find((c: any) => c.componentName === 'compiler');
|
||||
expect(compiler).toBeTruthy();
|
||||
expect(compiler.componentState.compiler).toBe('g7snapshot');
|
||||
expect(compiler.componentState.options).toBe('-std=c++1z -O3 ');
|
||||
|
||||
const filters = compiler.componentState.filters;
|
||||
expect(filters.commentOnly).toBe(true);
|
||||
expect(filters.directives).toBe(true);
|
||||
expect(filters.intel).toBe(true);
|
||||
expect(filters.labels).toBe(true);
|
||||
});
|
||||
|
||||
it('should deserialize GCC 4.7.4 with compressed source', () => {
|
||||
// From etc/oldhash.txt - compressed with lzstring
|
||||
const urlHash =
|
||||
'compilers:!((compiler:g474,options:%27%27,sourcez:PQKgBALgpgzhYHsBmYDGCC2AHATrGAlggHYB0pamGUx8AFlHmEgjmADY0DmEdAXACgAhiNFjxEyVOkzZw2QsVLl85WvVrVG7TolbdB7fsMmlx0xennLNsddu37Dy0%2BenXbwx8%2B7vPo/7OfoGaIMACAgS0YBhCUQAUUfBCOFyoADSUxHBodClgICApXABuAJRgAN4CYGB4EACuOMRgAIwATADcAgC%2BQAA)),filterAsm:(binary:!t,colouriseAsm:!t,commentOnly:!t,directives:!t,intel:!t,labels:!t),version:3';
|
||||
|
||||
const state = deserialiseState(urlHash);
|
||||
|
||||
expect(state).toBeTruthy();
|
||||
expect(state.version).toBe(4);
|
||||
expect(state.content).toBeTruthy();
|
||||
expect(Array.isArray(state.content)).toBe(true);
|
||||
|
||||
const row = state.content[0];
|
||||
expect(row.type).toBe('row');
|
||||
expect(Array.isArray(row.content)).toBe(true);
|
||||
expect(row.content.length).toBe(2);
|
||||
|
||||
const editor = row.content.find((c: any) => c.componentName === 'codeEditor');
|
||||
expect(editor).toBeTruthy();
|
||||
expect(editor.componentState.source).toBeTruthy();
|
||||
expect(typeof editor.componentState.source).toBe('string');
|
||||
expect(editor.componentState.source.length).toBeGreaterThan(0);
|
||||
expect(editor.componentState.source).toContain('int');
|
||||
expect(editor.componentState.options.colouriseAsm).toBe(true);
|
||||
|
||||
const compiler = row.content.find((c: any) => c.componentName === 'compiler');
|
||||
expect(compiler).toBeTruthy();
|
||||
expect(compiler.componentState.compiler).toBe('g474');
|
||||
expect(compiler.componentState.options).toBe('');
|
||||
|
||||
const filters = compiler.componentState.filters;
|
||||
expect(filters.binary).toBe(true);
|
||||
expect(filters.commentOnly).toBe(true);
|
||||
expect(filters.directives).toBe(true);
|
||||
expect(filters.intel).toBe(true);
|
||||
expect(filters.labels).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid/Edge Cases', () => {
|
||||
it('should handle empty string', () => {
|
||||
const state = deserialiseState('');
|
||||
expect(state).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle invalid JSON', () => {
|
||||
expect(() => {
|
||||
deserialiseState('%7Binvalid');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should handle invalid rison', () => {
|
||||
expect(() => {
|
||||
deserialiseState('invalid:rison:data');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should handle corrupt lzstring data', () => {
|
||||
// State with 'z' field but invalid base64
|
||||
expect(() => {
|
||||
deserialiseState('(z:invalid_base64_data)');
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,10 +22,9 @@
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import GoldenLayout from 'golden-layout';
|
||||
import lzstring from 'lz-string';
|
||||
import _ from 'underscore';
|
||||
import * as rison from '../shared/rison.js';
|
||||
import * as urlSerialization from '../shared/url-serialization.js';
|
||||
import * as Components from './components.js';
|
||||
|
||||
export function convertOldState(state: any): any {
|
||||
@@ -67,7 +66,7 @@ export function loadState(state: any): any {
|
||||
state = convertOldState(state);
|
||||
break; // no fall through
|
||||
case 4:
|
||||
state = GoldenLayout.unminifyConfig(state);
|
||||
state = urlSerialization.unminifyConfig(state);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid version '" + state.version + "'");
|
||||
@@ -75,26 +74,18 @@ export function loadState(state: any): any {
|
||||
return state;
|
||||
}
|
||||
|
||||
export function risonify(obj: rison.JSONValue): string {
|
||||
return rison.quote(rison.encode_object(obj));
|
||||
}
|
||||
|
||||
export function unrisonify(text: string): any {
|
||||
return rison.decode_object(decodeURIComponent(text.replace(/\+/g, '%20')));
|
||||
}
|
||||
|
||||
export function deserialiseState(stateText: string): any {
|
||||
let state;
|
||||
let exception;
|
||||
try {
|
||||
state = unrisonify(stateText);
|
||||
state = urlSerialization.unrisonify(stateText);
|
||||
if (state?.z) {
|
||||
const data = lzstring.decompressFromBase64(state.z);
|
||||
// If lzstring fails to decompress this it'll return an empty string rather than throwing an error
|
||||
if (data === '') {
|
||||
throw new Error('lzstring decompress error, url is corrupted');
|
||||
}
|
||||
state = unrisonify(data);
|
||||
state = urlSerialization.unrisonify(data);
|
||||
}
|
||||
} catch (ex) {
|
||||
exception = ex;
|
||||
@@ -112,15 +103,3 @@ export function deserialiseState(stateText: string): any {
|
||||
if (exception) throw exception;
|
||||
return loadState(state);
|
||||
}
|
||||
|
||||
export function serialiseState(stateText: any): string {
|
||||
const ctx = GoldenLayout.minifyConfig({content: stateText.content});
|
||||
ctx.version = 4;
|
||||
const uncompressed = risonify(ctx);
|
||||
const compressed = risonify({z: lzstring.compressToBase64(uncompressed)});
|
||||
const MinimalSavings = 0.2; // at least this ratio smaller
|
||||
if (compressed.length < uncompressed.length * (1.0 - MinimalSavings)) {
|
||||
return compressed;
|
||||
}
|
||||
return uncompressed;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ import GoldenLayout from 'golden-layout';
|
||||
import $ from 'jquery';
|
||||
import {assert, unwrap, unwrapString} from '../../shared/assert.js';
|
||||
import {escapeHTML} from '../../shared/common-utils.js';
|
||||
import {serialiseState} from '../../shared/url-serialization.js';
|
||||
import {SiteTemplateConfiguration, UserSiteTemplate} from '../../types/features/site-templates.interfaces.js';
|
||||
import * as BootstrapUtils from '../bootstrap-utils.js';
|
||||
import {localStorage} from '../local.js';
|
||||
import {Settings} from '../settings.js';
|
||||
import * as url from '../url.js';
|
||||
import {getStaticImage} from '../utils';
|
||||
import {Alert} from './alert.js';
|
||||
|
||||
@@ -54,7 +54,7 @@ class SiteTemplatesWidget {
|
||||
}
|
||||
saveCurrentAsTemplate() {
|
||||
const config = this.layout.toConfig();
|
||||
const data = url.serialiseState(config);
|
||||
const data = serialiseState(config);
|
||||
this.alertSystem.enterSomething('Template Name', '', '', {
|
||||
yes: name => {
|
||||
const userTemplates: Record<string, UserSiteTemplate> = JSON.parse(
|
||||
|
||||
Reference in New Issue
Block a user