mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-07-16 18:37:07 -04:00
Don't rewrite the URL in embedded mode; fixes embeds regressing to the front page (#8897)
Fixes #8896. ### What was happening `SharingBase.ensureUrlIsNotOutdated()` replaces the page URL with `httpRoot` (`/`) once the layout state diverges from the state recorded after load. That rewrite exists for the main site, so an outdated shortlink path (`/z/...`) doesn't linger in the address bar after you edit the code. Since #8166 started running the shared state-tracking (`SharingBase`) in embedded mode too — to keep the "Edit on Compiler Explorer" link in sync — the rewrite also fires inside `/e` iframes. There, `location.pathname` (`/e`) never equals `httpRoot` (`/`), so the first state change after load (an edit, or just layout churn) silently rewrites the iframe's URL to `/` — dropping both the `/e` path **and** the state hash. Reproduced with a `history.replaceState` spy: after two edits in an embed, the iframe's location is `/` with an empty hash. The visible symptoms are then browser-dependent, which is why it looked intermittent: Firefox restores an iframe's *session-history* URL when the parent page reloads, so the iframe comes back as `godbolt.org/` — the full site, showing the default language example, and (because cookies are unreadable in a cross-site iframe, per the `privacy_status` SameSite console error in the report) the privacy-policy popup. Chrome reloads iframes from their `src` attribute, so it never showed the problem. ### The fix Skip the rewrite entirely in embedded mode — the iframe's URL *is* the embedded state and must never be touched. The `a.link` "Edit on Compiler Explorer" state tracking from #8166 is unaffected. ### Tests New `cypress/e2e/embed.cy.ts`: - the embedded state's code loads (not the default example), - the `/e#state` URL survives state changes (fails before this fix: pathname becomes `/`, hash emptied), - the "Edit on Compiler Explorer" link still picks up edits (locks in #8166's behaviour). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mattgodbolt-molty <mattgodbolt-molty@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
17d7ff668f
commit
132f308ea4
113
cypress/e2e/embed.cy.ts
Normal file
113
cypress/e2e/embed.cy.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2026, 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 {serialiseState} from '../../shared/url-serialization.js';
|
||||
import {
|
||||
assertNoConsoleOutput,
|
||||
monacoEditorTextShouldContain,
|
||||
setMonacoEditorContent,
|
||||
stubConsoleOutput,
|
||||
} from '../support/utils';
|
||||
|
||||
// A minimal editor + compiler layout, like the ones generated by "Share > Embed in iframe".
|
||||
function buildEmbeddedState(source: string) {
|
||||
return {
|
||||
version: 4,
|
||||
content: [
|
||||
{
|
||||
type: 'row',
|
||||
content: [
|
||||
{
|
||||
type: 'component',
|
||||
componentName: 'codeEditor',
|
||||
componentState: {id: 1, lang: 'c++', source},
|
||||
isClosable: true,
|
||||
},
|
||||
{
|
||||
type: 'component',
|
||||
componentName: 'compiler',
|
||||
componentState: {compiler: 'gdefault', id: 1, lang: 'c++', source: 1},
|
||||
isClosable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const EMBEDDED_SOURCE = 'int embedded_marker(int num) {\n return num * 3;\n}';
|
||||
|
||||
// Embedded panes have no tab headers (hasHeaders: false), so we can't use the
|
||||
// lm_title-based helpers; the source editor is the first Monaco editor created.
|
||||
function embeddedSourceEditor() {
|
||||
return cy.get('.monaco-editor').first();
|
||||
}
|
||||
|
||||
describe('Embedded mode', () => {
|
||||
let stateHash: string;
|
||||
|
||||
beforeEach(() => {
|
||||
stateHash = serialiseState(buildEmbeddedState(EMBEDDED_SOURCE));
|
||||
cy.visit(`/e#${stateHash}`, {
|
||||
onBeforeLoad: win => {
|
||||
stubConsoleOutput(win);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return cy.window().then(_win => {
|
||||
assertNoConsoleOutput();
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the embedded code rather than the default example', () => {
|
||||
monacoEditorTextShouldContain(embeddedSourceEditor(), 'embedded_marker');
|
||||
});
|
||||
|
||||
it('keeps the /e path and state hash in the URL across state changes', () => {
|
||||
// Regression test for #8896: ensureUrlIsNotOutdated() used to rewrite the
|
||||
// iframe URL to httpRoot ("/") once the layout state diverged from the
|
||||
// state recorded after load, dropping both the /e path and the #hash.
|
||||
// Browsers that restore an iframe's session-history URL when the parent
|
||||
// page reloads (Firefox) then loaded the full site instead of the embed:
|
||||
// default example code plus, cross-site cookies being unreadable, the
|
||||
// privacy policy popup.
|
||||
monacoEditorTextShouldContain(embeddedSourceEditor(), 'embedded_marker');
|
||||
|
||||
// Edit the source twice, each time waiting for the "Edit on Compiler
|
||||
// Explorer" link to pick up the new state. The link is updated by the
|
||||
// same stateChanged handler that (before the fix) rewrote the URL, so
|
||||
// once the second distinct state has been observed in the link, the
|
||||
// buggy rewrite would definitely have happened.
|
||||
setMonacoEditorContent('int changed_marker(int num) {\n return num * 4;\n}');
|
||||
cy.get('a.link').should('have.attr', 'href').and('include', 'changed_marker');
|
||||
|
||||
setMonacoEditorContent('int changed_again(int num) {\n return num * 5;\n}');
|
||||
cy.get('a.link').should('have.attr', 'href').and('include', 'changed_again');
|
||||
|
||||
cy.location('pathname').should('equal', '/e');
|
||||
cy.location('hash').should('equal', `#${stateHash}`);
|
||||
});
|
||||
});
|
||||
@@ -112,6 +112,11 @@ export class SharingBase {
|
||||
}
|
||||
|
||||
protected ensureUrlIsNotOutdated(config: any): void {
|
||||
// Never rewrite the URL in embedded mode: doing so strips the /e path and
|
||||
// state hash from the iframe's URL, and browsers that restore an iframe's
|
||||
// session-history URL when the parent page reloads (e.g. Firefox) then
|
||||
// load the full site with the default example instead of the embed (#8896).
|
||||
if (options.embedded) return;
|
||||
const stringifiedConfig = JSON.stringify(config);
|
||||
if (stringifiedConfig !== this.lastState) {
|
||||
if (this.lastState != null && window.location.pathname !== window.httpRoot) {
|
||||
|
||||
Reference in New Issue
Block a user