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:
Matt Godbolt (bot acct)
2026-07-06 17:17:10 -05:00
committed by GitHub
parent 17d7ff668f
commit 132f308ea4
2 changed files with 118 additions and 0 deletions

113
cypress/e2e/embed.cy.ts Normal file
View 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}`);
});
});

View File

@@ -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) {