import { expect } from '@playwright/test';
import { testFactory, waitForHydrate } from './test-utils.js';

const test = testFactory({ root: './fixtures/view-transitions/' });

let devServer;

test.beforeAll(async ({ astro }) => {
	devServer = await astro.startDevServer();
});

test.afterAll(async () => {
	await devServer.stop();
});

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

test.describe('View Transitions', () => {
	test('Moving from page 1 to page 2', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 ViewTransitions triggers a full page navigation', async ({
		page,
		astro,
	}) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 ViewTransitions 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 ViewTransitions does not trigger a full page navigation', async ({
		page,
		astro,
	}) => {
		const loads = [];
		page.addListener('load', async (p) => {
			loads.push(p.title());
		});
		// 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 ViewTransitions 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');

		// check that we are further down the page
		const Y = await page.evaluate(() => window.scrollY);
		expect(Y, 'The target is further down the page').toBeGreaterThan(0);

		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 ViewTransitions w/ back button', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 ViewTransitions 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');
	});

	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 drakmode)
		// 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()

		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 = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});
		// 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();
	});

	test('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)
		await page.goForward();
		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('<Image /> component forwards transitions to the <img>', 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('<video> can persist using transition:persist', async ({ page, astro }) => {
		const getTime = () => document.querySelector('video').currentTime;

		// Go to page 1
		await page.goto(astro.resolveUrl('/video-one'));
		const vid = page.locator('video');
		await expect(vid).toBeVisible();
		const firstTime = await page.evaluate(getTime);

		// Navigate to page 2
		await page.click('#click-two');
		const p = page.locator('#video-two');
		await expect(p).toBeVisible();
		const secondTime = await page.evaluate(getTime);

		expect(secondTime).toBeGreaterThanOrEqual(firstTime);
	});

	test('Islands can persist using transition:persist', async ({ page, astro }) => {
		// Go to page 1
		await page.goto(astro.resolveUrl('/island-one'));
		let cnt = page.locator('.counter pre');
		await expect(cnt).toHaveText('5');

		await page.click('.increment');
		await expect(cnt).toHaveText('6');

		// Navigate to page 2
		await page.click('#click-two');
		const p = page.locator('#island-two');
		await expect(p).toBeVisible();
		cnt = page.locator('.counter pre');
		// Count should remain
		await expect(cnt).toHaveText('6');
	});

	test('Scripts are only executed once', 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');

		const meta = page.locator('[name="script-executions"]');
		await expect(meta).toHaveAttribute('content', '0');
	});

	test('Navigating to the same path but with different query params should result in transition', async ({
		page,
		astro,
	}) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// Go to page 1
		await page.goto(astro.resolveUrl('/query'));
		let p = page.locator('#query-page');
		await expect(p, 'should have content').toHaveText('Page 1');

		// go to page 2
		await page.click('#click-two');
		p = page.locator('#query-page');
		await expect(p, 'should have content').toHaveText('Page 2');

		await expect(loads.length, 'There should only be 1 page load').toEqual(1);
	});

	test('Importing ViewTransitions w/o using the component must not mess with history', async ({
		page,
		astro,
	}) => {
		const loads = [];
		page.addListener('load', async (p) => {
			loads.push(p);
		});
		// Go to the half bakeed page
		await page.goto(astro.resolveUrl('/half-baked'));
		let p = page.locator('#half-baked');
		await expect(p, 'should have content').toHaveText('Half Baked');

		// click a hash link to navigate further down the page
		await page.click('#click-hash');
		// still on page
		p = page.locator('#half-baked');
		await expect(p, 'should have content').toHaveText('Half Baked');

		// go back within same page without reloading
		await page.goBack();
		p = page.locator('#half-baked');
		await expect(p, 'should have content').toHaveText('Half Baked');

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for going back on same page'
		).toEqual(1);
	});

	test('Navigation also swaps the attributes of the document root', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/some-attributes'));
		let p = page.locator('#heading');
		await expect(p, 'should have content').toHaveText('Page with some attributes');

		let h = page.locator('html');
		await expect(h, 'should have content').toHaveAttribute('lang', 'en');

		await page.click('#click-other-attributes');
		p = page.locator('#heading');
		await expect(p, 'should have content').toHaveText('Page with other attributes');

		h = page.locator('html');
		await expect(h, 'should have content').toHaveAttribute('lang', 'es');
		await expect(h, 'should have content').toHaveAttribute('style', 'background-color: green');
		await expect(h, 'should have content').toHaveAttribute('data-other-name', 'value');
		await expect(h, 'should have content').toHaveAttribute('data-astro-fake', 'value');
		await expect(h, 'should have content').toHaveAttribute('data-astro-transition', 'forward');
		await expect(h, 'should be absent').not.toHaveAttribute('class', /.*/);
	});

	test('Link with data-astro-reload attribute should trigger page load, no tranistion', async ({
		page,
		astro,
	}) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 2
		await page.click('#click-two');
		p = page.locator('#two');
		await expect(p, 'should have content').toHaveText('Page 2');

		expect(loads.length, 'There should be 2 page load').toEqual(2);
	});

	test('Link with download attribute should trigger download, no transition', async ({
		page,
		astro,
	}) => {
		// Go to page 4
		await page.goto(astro.resolveUrl('/four'));
		let p = page.locator('#four');
		await expect(p, 'should have content').toHaveText('Page 4');

		// Start waiting for download before clicking. Note no await.
		const downloadPromise = page.waitForEvent('download', { timeout: 4000 });
		await page.click('#click-logo');
		await downloadPromise;
	});

	test('data-astro-reload not required for non-html content', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});
		// Go to page 4
		await page.goto(astro.resolveUrl('/four'));
		let p = page.locator('#four');
		await expect(p, 'should have content').toHaveText('Page 4');

		await page.click('#click-svg');
		p = page.locator('svg');
		await expect(p).toBeVisible();
		expect(loads.length, 'There should be 2 page load').toEqual(2);
	});

	test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({
		page,
		astro,
	}) => {
		// Go to middle of long page
		await page.goto(astro.resolveUrl('/long-page#click-external'));

		let locator = page.locator('#click-external');
		await expect(locator).toBeInViewport();

		// Go to a page that has not enabled ViewTransistions
		await page.click('#click-external');
		locator = page.locator('#three');
		await expect(locator).toHaveText('Page 3');

		// Scroll back to long page
		await page.goBack();
		locator = page.locator('#click-external');
		await expect(locator).toBeInViewport();
	});

	test("Non transition navigation doesn't loose handlers", 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 3
		await page.click('#click-three');
		p = page.locator('#three');
		await expect(p, 'should have content').toHaveText('Page 3');

		// go to page 5
		await page.click('#click-five');
		p = page.locator('#five');
		await expect(p, 'should have content').toHaveText('Page 5');

		await page.goBack();
		p = page.locator('#three');
		await expect(p, 'should have content').toHaveText('Page 3');

		await page.goBack();
		p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');
	});

	test('Moving to a page which redirects to another', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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-redirect-two');
		p = page.locator('#two');
		await expect(p, 'should have content').toHaveText('Page 2');

		// go back
		await page.goBack();
		p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');

		expect(
			loads.length,
			'There should only be the initial page load and two normal transitions'
		).toEqual(1);
	});

	test('Redirect to external site causes page load', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 external page
		await page.click('#click-redirect-external');
		// doesn't work for playwright when we are too fast
		p = page.locator('h1');

		await expect(p, 'should have content').toBeVisible();
		await page.waitForURL('http://example.com');
		await page.waitForFunction((arr) => arr.length === 2, loads);
		expect(loads.length, 'There should be 2 page loads').toEqual(2);
	});

	test('client:only styles are retained on transition (1/2)', async ({ page, astro }) => {
		const totalExpectedStyles = 9;

		await page.goto(astro.resolveUrl('/client-only-one'));
		let msg = page.locator('.counter-message');
		await expect(msg).toHaveText('message here');

		let styles = await page.locator('style').all();
		expect(styles.length).toEqual(totalExpectedStyles);

		await page.click('#click-two');

		let pageTwo = page.locator('#page-two');
		await expect(pageTwo, 'should have content').toHaveText('Page 2');

		styles = await page.locator('style').all();
		expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed');
	});

	test('client:only styles are retained on transition (2/2)', async ({ page, astro }) => {
		const totalExpectedStyles_page_three = 11;
		const totalExpectedStyles_page_four = 9;

		await page.goto(astro.resolveUrl('/client-only-three'));
		let msg = page.locator('#name');
		await expect(msg).toHaveText('client-only-three');
		await waitForHydrate(page, page.getByText('Vue'));
		await waitForHydrate(page, page.getByText('Svelte'));

		let styles = await page.locator('style').all();
		expect(styles.length).toEqual(totalExpectedStyles_page_three);

		await page.click('#click-four');

		let pageTwo = page.locator('#page-four');
		await expect(pageTwo, 'should have content').toHaveText('Page 4');

		styles = await page.locator('style').all();
		expect(styles.length).toEqual(totalExpectedStyles_page_four, 'style count has not changed');
	});

	test('Horizontal scroll position restored on back button', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/wide-page'));
		let article = page.locator('#widepage');
		await expect(article, 'should have script content').toBeVisible('exists');

		let locator = page.locator('#click-one');
		await expect(locator).not.toBeInViewport();

		await page.click('#click-right');
		locator = page.locator('#click-one');
		await expect(locator).toBeInViewport();
		locator = page.locator('#click-top');
		await expect(locator).toBeInViewport();

		await page.click('#click-one');
		let p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');

		await page.goBack();
		locator = page.locator('#click-one');
		await expect(locator).toBeInViewport();

		locator = page.locator('#click-top');
		await expect(locator).toBeInViewport();

		await page.click('#click-top');
		locator = page.locator('#click-one');
		await expect(locator).not.toBeInViewport();
	});

	test('Use the client side router', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/six'));
		// page six loads the router and automatically uses the router to navigate to page 1
		let p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');

		// nudge to jump to page 2
		await page.evaluate(() => {
			window.dispatchEvent(new Event('jumpToTwo'));
		});
		p = page.locator('#two');
		await expect(p, 'should have content').toHaveText('Page 2');

		// jump to page 3
		await page.evaluate(() => {
			// get the router from its fixture park position
			const navigate = window.clientSideRouterForTestsParkedHere;
			navigate('/three');
		});
		p = page.locator('#three');
		await expect(p, 'should have content').toHaveText('Page 3');

		// go back
		await page.goBack();
		p = page.locator('#two');
		await expect(p, 'should have content').toHaveText('Page 2');

		// no bad things happen when we revisit redirecting to page 6
		await page.goto(astro.resolveUrl('/six'));
		p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');
	});

	test('Use the client side router in framework components', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/client-load'));

		// the button is set to naviagte() to /two
		const button = page.locator('#react-client-load-navigate-button');

		await expect(button, 'should have content').toHaveText('Navigate to `/two`');

		await button.click();

		const p = page.locator('#two');

		await expect(p, 'should have content').toHaveText('Page 2');
	});

	test('body inline scripts do not re-execute on navigation', async ({ page, astro }) => {
		const errors = [];
		page.addListener('pageerror', (err) => {
			errors.push(err);
		});

		await page.goto(astro.resolveUrl('/inline-script-one'));
		let article = page.locator('#counter');
		await expect(article, 'should have script content').toBeVisible('exists');

		await page.click('#click-one');

		article = page.locator('#counter');
		await expect(article, 'should have script content').toHaveText('Count: 3');

		expect(errors).toHaveLength(0);
	});

	test('replace history', async ({ page, astro }) => {
		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');

		// replace with long page
		await page.click('#click-longpage');
		let article = page.locator('#longpage');
		await expect(article, 'should have script content').toBeVisible('exists');

		// one step back == #1
		await page.goBack();
		p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');
	});

	test('CSR replace history', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/six'));
		// page six loads the router and automatically uses the router to navigate to page 1
		let p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');

		// goto #2
		await page.evaluate(() => {
			window.clientSideRouterForTestsParkedHere('/two', { history: 'auto' });
		});
		p = page.locator('#two');
		await expect(p, 'should have content').toHaveText('Page 2');

		// replace with long page
		await page.evaluate(() => {
			window.clientSideRouterForTestsParkedHere('/long-page', { history: 'replace' });
		});
		let article = page.locator('#longpage');
		await expect(article, 'should have script content').toBeVisible('exists');

		// one step back == #1
		await page.goBack();
		p = page.locator('#one');
		await expect(p, 'should have content').toHaveText('Page 1');
	});

	test('Keep focus on transition', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/page-with-persistent-form'));
		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Form 1');

		locator = page.locator('#input');
		await locator.type('Hello');
		await expect(locator).toBeFocused();
		await locator.press('Enter');

		await page.waitForURL(/.*name=Hello/);
		locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Form 1');
		locator = page.locator('#input');
		await expect(locator).toBeFocused();

		await locator.type(' World');
		await expect(locator).toHaveValue('Hello World');
	});

	test('form POST that redirects to another page is handled', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', async (p) => {
			loads.push(p);
		});

		await page.goto(astro.resolveUrl('/form-one'));

		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Contact Form');

		// Submit the form
		await page.click('#submit');
		const span = page.locator('#contact-name');
		await expect(span, 'should have content').toHaveText('Testing');

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for the form submission'
		).toEqual(1);
	});

	test('form POST that action for cross-origin is opt-out', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/form-five'));
		page.on('request', (request) => expect(request.method()).toBe('POST'));
		// Submit the form
		await page.click('#submit');
	});

	test('form GET that redirects to another page is handled', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', async (p) => {
			loads.push(p);
		});

		await page.goto(astro.resolveUrl('/form-one?method=get'));

		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Contact Form');

		// Submit the form
		await page.click('#submit');
		const span = page.locator('#contact-name');
		await expect(span, 'should have content').toHaveText('Testing');

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for the form submission'
		).toEqual(1);
	});

	test('form POST when there is an error shows the error', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', async (p) => {
			loads.push(p);
		});

		await page.goto(astro.resolveUrl('/form-one?throw'));

		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Contact Form');

		// Submit the form
		await page.click('#submit');
		const overlay = page.locator('vite-error-overlay');
		await expect(overlay).toBeVisible();

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for the form submission'
		).toEqual(1);
	});

	test('form POST defaults to multipart/form-data (Astro 4.x compatibility)', async ({
		page,
		astro,
	}) => {
		const loads = [];

		page.addListener('load', async (p) => {
			loads.push(p);
		});

		const postedEncodings = [];

		await page.route('**/contact', async (route) => {
			const request = route.request();

			if (request.method() === 'POST') {
				postedEncodings.push(request.headers()['content-type'].split(';')[0]);
			}

			await route.continue();
		});

		await page.goto(astro.resolveUrl('/form-one'));

		// Submit the form
		await page.click('#submit');

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for the form submission'
		).toEqual(1);

		expect(
			postedEncodings,
			'There should be 1 POST, with encoding set to `multipart/form-data`'
		).toEqual(['multipart/form-data']);
	});

	test('form POST respects enctype attribute', async ({ page, astro }) => {
		const loads = [];

		page.addListener('load', async (p) => {
			loads.push(p);
		});

		const postedEncodings = [];

		await page.route('**/contact', async (route) => {
			const request = route.request();

			if (request.method() === 'POST') {
				postedEncodings.push(request.headers()['content-type'].split(';')[0]);
			}

			await route.continue();
		});

		await page.goto(
			astro.resolveUrl(
				`/form-one?${new URLSearchParams({ enctype: 'application/x-www-form-urlencoded' })}`
			)
		);

		// Submit the form
		await page.click('#submit');

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for the form submission'
		).toEqual(1);

		expect(
			postedEncodings,
			'There should be 1 POST, with encoding set to `multipart/form-data`'
		).toEqual(['application/x-www-form-urlencoded']);
	});

	test('Route announcer is invisible on page transition', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/no-directive-one'));

		let locator = page.locator('#one');
		await expect(locator, 'should have content').toHaveText('One');

		await page.click('a');
		locator = page.locator('#two');
		await expect(locator, 'should have content').toHaveText('Two');

		let announcer = page.locator('.astro-route-announcer');
		await expect(announcer, 'should have content').toHaveCSS('width', '1px');
	});

	test('should prefetch on hover by default', async ({ page, astro }) => {
		/** @type {string[]} */
		const reqUrls = [];
		page.on('request', (req) => {
			reqUrls.push(new URL(req.url()).pathname);
		});
		await page.goto(astro.resolveUrl('/prefetch'));
		expect(reqUrls).not.toContainEqual('/one');
		await Promise.all([
			page.waitForEvent('request'), // wait prefetch request
			page.locator('#prefetch-one').hover(),
		]);
		expect(reqUrls).toContainEqual('/one');
	});

	test('form POST with no action handler', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', async (p) => {
			loads.push(p);
		});

		await page.goto(astro.resolveUrl('/form-two'));

		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Contact Form');

		// Submit the form
		await page.click('#submit');
		const span = page.locator('#contact-name');
		await expect(span, 'should have content').toHaveText('Testing');

		expect(
			loads.length,
			'There should be only 1 page load. No additional loads for the form submission'
		).toEqual(1);
	});

	test('forms are overridden by formmethod and formaction', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/form-three'));

		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Contact Form');

		// Submit the form
		await page.click('#submit');
		const result = page.locator('#three-result');
		await expect(result, 'should have content').toHaveText('Got: Testing');
	});

	test('click on an svg anchor should trigger navigation', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		await page.goto(astro.resolveUrl('/non-html-anchor'));
		let locator = page.locator('#insidesvga');
		await expect(locator, 'should have attribute').toHaveAttribute('x', '10');
		await page.click('#svga');
		const 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('click inside an svg anchor should trigger navigation', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});
		await page.goto(astro.resolveUrl('/non-html-anchor'));
		let locator = page.locator('#insidesvga');
		await expect(locator, 'should have content').toHaveText('text within a svga');
		await page.click('#insidesvga');
		const 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('click on an area in an image map should trigger navigation', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});
		await page.goto(astro.resolveUrl('/non-html-anchor'));
		let locator = page.locator('#area');
		await expect(locator, 'should have attribute').toHaveAttribute('shape', 'default');
		await page.click('#logo');
		const 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('Submitter with a name property is included in form data', async ({ page, astro }) => {
		await page.goto(astro.resolveUrl('/form-four'));

		let locator = page.locator('h2');
		await expect(locator, 'should have content').toHaveText('Voting Form');

		// Submit the form
		const expected = page.url() + '?stars=3';
		await page.click('#three');
		await expect(page).toHaveURL(expected);
	});

	test('Dialog using form with method of "dialog" should not trigger navigation', async ({
		page,
		astro,
	}) => {
		await page.goto(astro.resolveUrl('/dialog'));

		let requests = [];
		page.on('request', (request) => requests.push(`${request.method()} ${request.url()}`));

		await page.click('#open');
		await expect(page.locator('dialog')).toHaveAttribute('open');
		await page.click('#close');
		await expect(page.locator('dialog')).not.toHaveAttribute('open');

		expect(requests).toHaveLength(0);
	});

	test('view transition should also work with 404 page', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});

		// 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 404
		await page.click('#click-404');
		p = page.locator('#FourOhFour');
		await expect(p, 'should have content').toHaveText('Page not found');

		expect(loads.length, 'There should only be 1 page load').toEqual(1);
	});

	test('custom elements can trigger a view transition', async ({ page, astro }) => {
		const loads = [];
		page.addListener('load', (p) => {
			loads.push(p.title());
		});
		await page.goto(astro.resolveUrl('/one'));
		await expect(page.locator('#one'), 'should have content').toHaveText('Page 1');
		// go to page 2
		await page.click('#custom-click-two');
		await expect(page.locator('#two'), 'should have content').toHaveText('Page 2');

		expect(loads.length, 'There should only be 1 page load').toEqual(1);
	});

	test('transition:name should be escaped correctly', async ({ page, astro }) => {
		const expectedAnimations = new Set();
		const checkName = async (selector, name) => {
			expectedAnimations.add(name);
			expect(page.locator(selector), 'should be escaped correctly').toHaveCSS(
				'view-transition-name',
				name
			);
		};

		page.on('console', (msg) => {
			if (msg.text().startsWith('anim: ')) {
				const split = msg.text().split(' ', 2);
				expectedAnimations.delete(split[1]);
			}
		});

		await page.goto(astro.resolveUrl('/transition-name'));

		await checkName('#one', 'front-end');
		await checkName('#two', '开源');
		await checkName('#three', '开a源');
		await checkName('#four', 'c开a源c');
		await checkName('#five', 'オープンソース');
		await checkName('#six', '开_24源');
		await checkName('#seven', '开_2e源');
		await checkName('#eight', '🐎👱❤');
		await checkName('#nine', '_--9');
		await checkName('#ten', '_10');
		await checkName('#eleven', '_-11');
		await checkName('#twelve', '__23_21_20_2f');
		await checkName('#thirteen', '___01____02______');
		await checkName(
			'#batch0',
			'__00_01_02_03_04_05_06_07_08_09_0a_0b_0c_0d_0e_0f_10_11_12_13_14_15_16_17_18_19_1a_1b_1c_1d_1e_1f'
		);
		await checkName(
			'#batch1',
			'__20_21_22_23_24_25_26_27_28_29_2a_2b_2c-_2e_2f0123456789_3a_3b_3c_3d_3e_3f'
		);
		await checkName('#batch2', '__40ABCDEFGHIJKLMNOPQRSTUVWXYZ_5b_5c_5d_5e__');
		await checkName('#batch3', '__60abcdefghijklmnopqrstuvwxyz_7b_7c_7d_7e_7f');
		await checkName(
			'#batch4',
			'\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f'
		);
		await checkName(
			'#batch5',
			'\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf'
		);
		await checkName(
			'#batch6',
			'\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf'
		);
		await checkName(
			'#batch7',
			'\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
		);

		await page.click('#navigate');
		await page.waitForTimeout(400); // yes, I dislike this, too. Might fix later.
		expect(
			expectedAnimations.size,
			'all animations for transition:names should have been found'
		).toEqual(0);
	});
});
