diff --git a/.changeset/six-ducks-sit.md b/.changeset/six-ducks-sit.md
new file mode 100644
index 000000000000..3a80ecd8ccac
--- /dev/null
+++ b/.changeset/six-ducks-sit.md
@@ -0,0 +1,11 @@
+---
+'astro': patch
+---
+
+**BREAKING CHANGE to the experimental Content Security Policy feature only**
+
+The `ClientRouter` component doesn't support CSP anymore. Supporting CSP meant to
+to make the underling implementation of view transition asynchronous, which caused
+some breaking changes to users.
+
+The support might be introduced in future releases.
diff --git a/packages/astro/e2e/csp-view-transitions.test.js b/packages/astro/e2e/csp-view-transitions.test.js
deleted file mode 100644
index c7f3ac9aa0f5..000000000000
--- a/packages/astro/e2e/csp-view-transitions.test.js
+++ /dev/null
@@ -1,1626 +0,0 @@
-import { expect } from '@playwright/test';
-import { testFactory } from './test-utils.js';
-
-const test = testFactory(import.meta.url, {
- root: './fixtures/view-transitions/',
- experimental: {
- csp: true,
- },
-});
-
-let previewServer;
-
-test.beforeAll(async ({ astro }) => {
- await astro.build();
- previewServer = await astro.preview();
-});
-
-test.afterAll(async () => {
- await previewServer.stop();
-});
-function collectLoads(page) {
- const loads = [];
- page.on('load', async () => {
- const url = page.url();
- if (url !== 'about:blank') loads.push(await page.title());
- });
- return loads;
-}
-function scrollToBottom(page) {
- return page.evaluate(() => {
- window.scrollY = document.documentElement.scrollHeight;
- window.dispatchEvent(new Event('scroll'));
- });
-}
-
-function collectPreloads(page) {
- return page.evaluate(() => {
- window.preloads = [];
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) =>
- mutation.addedNodes.forEach((node) => {
- if (node.nodeName === 'LINK' && node.rel === 'preload') preloads.push(node.href);
- }),
- );
- });
- observer.observe(document.head, { childList: true });
- });
-}
-
-async function nativeViewTransition(page) {
- return page.evaluate(() => document.startViewTransition !== undefined);
-}
-
-test.describe('CSP View Transitions', () => {
- test('Moving from page 1 to page 2', async ({ page, astro }) => {
- const loads = collectLoads(page);
-
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // go to page 2
- await page.click('#click-two');
- p = page.locator('#two');
- await expect(p, 'should have content').toHaveText('Page 2');
-
- expect(loads.length, 'There should only be 1 page load').toEqual(1);
- });
-
- test('Back button is captured', async ({ page, astro }) => {
- const loads = collectLoads(page);
-
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // go to page 2
- await page.click('#click-two');
- p = page.locator('#two');
- await expect(p, 'should have content').toHaveText('Page 2');
-
- // Back to page 1
- await page.goBack();
- p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- expect(loads.length, 'There should only be 1 page load').toEqual(1);
- });
-
- test('Clicking on a link with nested content', async ({ page, astro }) => {
- const loads = collectLoads(page);
- // Go to page 4
- await page.goto(astro.resolveUrl('/four'));
- let p = page.locator('#four');
- await expect(p, 'should have content').toHaveText('Page 4');
-
- // Go to page 1
- await page.click('#click-one');
- p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- expect(loads.length, 'There should only be 1 page load').toEqual(1);
- });
-
- test('Clicking on a link to a page with non-recommended headers', async ({ page, astro }) => {
- const loads = collectLoads(page);
- // Go to page 4
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Go to page 1
- await page.click('#click-seven');
- p = page.locator('#seven');
- await expect(p, 'should have content').toHaveText('Page 7');
-
- expect(loads.length, 'There should only be 1 page load').toEqual(1);
- });
-
- test('Moving to a page without ClientRouter triggers a full page navigation', async ({
- page,
- astro,
- }) => {
- const loads = collectLoads(page);
-
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Go to page 3 which does *not* have ClientRouter enabled
- await page.click('#click-three');
- p = page.locator('#three');
- await expect(p, 'should have content').toHaveText('Page 3');
-
- expect(
- loads.length,
- 'There should be 2 page loads. The original, then going from 3 to 2',
- ).toEqual(2);
- });
-
- test('Moving within a page without ClientRouter does not trigger a full page navigation', async ({
- page,
- astro,
- }) => {
- const loads = collectLoads(page);
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Go to page 3 which does *not* have ClientRouter enabled
- await page.click('#click-three');
- p = page.locator('#three');
- await expect(p, 'should have content').toHaveText('Page 3');
-
- // click a hash link to navigate further down the page
- await page.click('#click-hash');
- // still on page 3
- p = page.locator('#three');
- await expect(p, 'should have content').toHaveText('Page 3');
-
- expect(
- loads.length,
- 'There should be only 2 page loads (for page one & three), but no additional loads for the hash change',
- ).toEqual(2);
- });
-
- test('Moving from a page without ClientRouter w/ back button', async ({ page, astro }) => {
- const loads = collectLoads(page);
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Go to page 3 which does *not* have ClientRouter enabled
- await page.click('#click-three');
- p = page.locator('#three');
- await expect(p, 'should have content').toHaveText('Page 3');
-
- // Back to page 1
- await page.goBack();
- p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
- expect(
- loads.length,
- 'There should be 3 page loads (for page one & three), and an additional loads for the back navigation',
- ).toEqual(3);
- });
-
- test('Stylesheets in the head are waited on', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- await collectPreloads(page);
-
- // Go to page 2
- await page.click('#click-two');
- p = page.locator('#two');
- await expect(p, 'should have content').toHaveText('Page 2');
- await expect(p, 'imported CSS updated').toHaveCSS('font-size', '24px');
- const preloads = await page.evaluate(() => window.preloads);
- expect(preloads.length === 1 && preloads[0].endsWith('/two.css')).toBeTruthy();
- });
-
- test('astro:page-load event fires when navigating to new page', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- const p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // go to page 2
- await page.click('#click-two');
- const article = page.locator('#twoarticle');
- await expect(article, 'should have script content').toHaveText('works');
- });
-
- test('astro:page-load event fires when navigating directly to a page', async ({
- page,
- astro,
- }) => {
- // Go to page 2
- await page.goto(astro.resolveUrl('/two'));
- const article = page.locator('#twoarticle');
- await expect(article, 'should have script content').toHaveText('works');
- });
-
- test('astro:after-swap event fires right after the swap', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // go to page 2
- await page.click('#click-two');
- p = page.locator('#two');
- const h = page.locator('html');
- await expect(h, 'imported CSS updated').toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
- });
-
- test('No page rendering during swap()', async ({ page, astro }) => {
- // This has been a problem with theme switchers (e.g. for darkmode)
- // Swap() should not trigger any page renders and give users the chance to
- // correct attributes in the astro:after-swap handler before they become visible
-
- // This test uses a CSS animation to detect page rendering
- // The test succeeds if no additional animation beside those of the
- // view transition is triggered during swap()
-
- // Only works for browsers with native view transitions
- if (!(await nativeViewTransition(page))) return;
-
- await page.goto(astro.resolveUrl('/listener-one'));
- let p = page.locator('#totwo');
- await expect(p, 'should have content').toHaveText('Go to listener two');
-
- // setting the blue class on the html element triggers a CSS animation
- let animations = await page.evaluate(async () => {
- document.documentElement.classList.add('blue');
- return document.getAnimations();
- });
- expect(animations.length).toEqual(1);
-
- // go to page 2
- await page.click('#totwo');
- p = page.locator('#toone');
- await expect(p, 'should have content').toHaveText('Go to listener one');
- // swap() resets the "blue" class, as it is not set in the static html of page 2
- // The astro:after-swap listener (defined in the layout) sets it to "blue" again.
- // The temporarily missing class must not trigger page rendering.
-
- // When the after-swap listener starts, no animations should be running
- // after-swap listener sets animations to document.getAnimations().length
- // and we expect this to be zero
- await expect(page.locator('html')).toHaveAttribute('animations', '0');
- });
-
- test('click hash links does not do navigation', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- const p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Clicking 1 stays put
- await page.click('#click-one');
- await expect(p, 'should have content').toHaveText('Page 1');
- });
-
- test('click self link (w/o hash) does not do navigation', async ({ page, astro }) => {
- const loads = collectLoads(page);
-
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- const p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Clicking href="" stays on page
- await page.click('#click-self');
- await expect(p, 'should have content').toHaveText('Page 1');
- expect(loads.length, 'There should only be 1 page load').toEqual(1);
- });
-
- test('Scroll position restored on back button', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/long-page'));
- let article = page.locator('#longpage');
- await expect(article, 'should have script content').toBeVisible('exists');
-
- await scrollToBottom(page);
- const oldScrollY = await page.evaluate(() => window.scrollY);
-
- // go to page long-page
- await page.click('#click-one');
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // Back to page 1
- await page.goBack();
-
- const newScrollY = await page.evaluate(() => window.scrollY);
- expect(oldScrollY).toEqual(newScrollY);
- });
-
- test('Fragment scroll position restored on back button', async ({ page, astro }) => {
- // Go to the long page
- await page.goto(astro.resolveUrl('/long-page'));
- let locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
-
- // Scroll down to middle fragment
- await page.click('#click-scroll-down');
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
-
- // Scroll up to top fragment
- await page.click('#click-scroll-up');
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
-
- // Back to middle of the page
- await page.goBack();
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
- });
-
- test('Scroll position restored when transitioning back to fragment', async ({ page, astro }) => {
- // Go to the long page
- await page.goto(astro.resolveUrl('/long-page'));
- let locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
-
- // Scroll down to middle fragment
- await page.click('#click-scroll-down');
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
-
- // goto page 1
- await page.click('#click-one-again');
- locator = page.locator('#one');
- await expect(locator).toHaveText('Page 1');
-
- // Back to middle of the previous page
- await page.goBack();
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
- });
-
- test('Scroll position restored on forward button', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/one'));
- let p = page.locator('#one');
- await expect(p, 'should have content').toHaveText('Page 1');
-
- // go to page long-page
- await page.click('#click-longpage');
- let article = page.locator('#longpage');
- await expect(article, 'should have script content').toBeVisible('exists');
-
- await scrollToBottom(page);
- const oldScrollY = await page.evaluate(() => window.scrollY);
-
- // Back to page 1
- await page.goBack();
-
- // Go forward
- await page.goForward();
- article = page.locator('#longpage');
- await expect(article, 'should have script content').toBeVisible('exists');
-
- const newScrollY = await page.evaluate(() => window.scrollY);
- expect(oldScrollY).toEqual(newScrollY);
- });
-
- test('Fragment scroll position restored on forward button', async ({ page, astro }) => {
- // Go to the long page
- await page.goto(astro.resolveUrl('/long-page'));
- let locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
-
- // Scroll down to middle fragment
- await page.click('#click-scroll-down');
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
-
- // Scroll back to top
- await page.goBack();
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
-
- // Forward to middle of page
- await page.goForward();
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
- });
-
- // We don't support inline scripts yet
- test.skip('View Transitions Rule', async ({ page, astro }) => {
- let consoleCount = 0;
- page.on('console', (msg) => {
- // This count is used for transition events
- if (msg.text() === 'ready') consoleCount++;
- });
- // Don't test back and forward '' to '', because They are not stored in the history.
- // click '' to '' (transition)
- await page.goto(astro.resolveUrl('/long-page'));
- let locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- let consolePromise = page.waitForEvent('console');
- await page.click('#click-self');
- await consolePromise;
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // click '' to 'hash' (no transition)
- await page.click('#click-scroll-down');
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // back 'hash' to '' (no transition)
- await page.goBack();
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // forward '' to 'hash' (no transition)
- // NOTE: the networkidle below is needed for Firefox to consistently
- // pass the `#longpage` viewport check below
- await page.goForward({ waitUntil: 'networkidle' });
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // click 'hash' to 'hash' (no transition)
- await page.click('#click-scroll-up');
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // back 'hash' to 'hash' (no transition)
- await page.goBack();
- locator = page.locator('#click-one-again');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // forward 'hash' to 'hash' (no transition)
- await page.goForward();
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(1);
-
- // click 'hash' to '' (transition)
- consolePromise = page.waitForEvent('console');
- await page.click('#click-self');
- await consolePromise;
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(2);
-
- // back '' to 'hash' (transition)
- consolePromise = page.waitForEvent('console');
- await page.goBack();
- await consolePromise;
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(3);
-
- // forward 'hash' to '' (transition)
- consolePromise = page.waitForEvent('console');
- await page.goForward();
- await consolePromise;
- locator = page.locator('#longpage');
- await expect(locator).toBeInViewport();
- expect(consoleCount).toEqual(4);
- });
-
- test(' component forwards transitions to the
', async ({ page, astro }) => {
- // Go to page 1
- await page.goto(astro.resolveUrl('/image-one'));
- const img = page.locator('img[data-astro-transition-scope]');
- await expect(img).toBeVisible('The image tag should have the transition scope attribute.');
- });
-
- test('