Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 78 additions & 4 deletions e2e/tests/404.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
*/

import {test, expect} from '@playwright/test';
import {BASE_URL, expect404PageButtons, goTo404Page} from './utils';

test('Bad URL redirection to 404 page', async ({page}) => {
const badUrls = [
// Test for bad public asset
'http://localhost:5555/public/junk',
`${BASE_URL}/public/junk`,
// Test for bad URL goes to the not found component
'http://localhost:5555/bad_url',
`${BASE_URL}/bad_url`,
// TODO. Test for bad app urls (e.g. bad feature id)
];

Expand All @@ -35,12 +36,85 @@ test('Bad URL redirection to 404 page', async ({page}) => {

// Assert that the response status code is 404
expect(response.status()).toBe(404);

// Check page content
const errorMessage = page.locator('#error-detailed-message');
await expect(errorMessage).toBeVisible();
await expect(errorMessage).toContainText(
"We couldn't find the page you're looking for.",
);

// Check buttons
await expect(page.locator('#error-action-home-btn')).toBeVisible();
await expect(page.locator('#error-action-report')).toBeVisible();
});
}
});

test('matches the screenshot', async ({page}) => {
await page.goto('http://localhost:5555/bad_url');
test('shows similar features and all buttons when results exist', async ({
page,
}) => {
const query = 'g';
await goTo404Page(page, query);

await expect(page.locator('.similar-features-container')).toBeVisible();
await expect404PageButtons(page, {hasSearch: true});

const similarContainerButton = page.locator('#error-action-search-btn');
const pageContainer = page.locator('.page-container');

// Snapshot
await expect(pageContainer).toHaveScreenshot(
'not-found-error-page-similar-results.png',
);

// Clicking the search button should redirect to homepage with search
await Promise.all([page.waitForNavigation(), similarContainerButton.click()]);
await expect(page).toHaveURL(`${BASE_URL}?q=${query}`);
});

test('shows only home and report buttons when no similar features found', async ({
page,
}) => {
const query = 'nonexistent-feature';
await goTo404Page(page, query);

await expect(page.locator('.similar-features-container')).toHaveCount(0);
await expect404PageButtons(page, {hasSearch: false});

await expect(page.locator('#error-detailed-message')).toContainText(
`We could not find Feature ID: ${query}`,
);

await expect(page.locator('.error-message')).toContainText(
'No similar features found.',
);
});

test('should allow navigation from 404 page', async ({page}) => {
const badUrl = `${BASE_URL}/feature/doesNotExist123`;
await page.goto(badUrl);
await expect(page).toHaveURL(badUrl);

// Home button navigation
const homeButton = page.locator('#error-action-home-btn');
await expect(homeButton).toBeVisible();
await homeButton.click();
await expect(page).toHaveURL(BASE_URL);

await page.goBack();

// Report an issue button should be present
const reportButton = page.locator('#error-action-report');
await expect(reportButton).toBeVisible();
await expect(reportButton).toHaveAttribute(
'href',
'https://github.com/GoogleChrome/webstatus.dev/issues/new/choose',
);
});

test('matches the screenshot 404 not found page', async ({page}) => {
await page.goto(`${BASE_URL}/bad_url`);
const pageContainer = page.locator('.page-container');
await expect(pageContainer).toHaveScreenshot('not-found-error-page.png');
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions e2e/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {Page, expect} from '@playwright/test';

const DEFAULT_FAKE_NOW = 'Dec 1 2020 12:34:56';

export const BASE_URL = 'http://localhost:5555';

export async function setupFakeNow(
page: Page,
fakeNowDateString = DEFAULT_FAKE_NOW,
Expand Down Expand Up @@ -86,3 +88,27 @@ export async function loginAsUser(page: Page, username: string) {
await popup.getByText(username).click();
await popup.waitForEvent('close');
}

export async function goTo404Page(page, query: string): Promise<void> {
await page.goto(`${BASE_URL}/features/${query}`);
await expect(page).toHaveURL(
`${BASE_URL}/errors-404/feature-not-found?q=${query}`,
);

const response = await page.context().request.fetch(page.url());
expect(response.status()).toBe(404);
}

export async function expect404PageButtons(
page,
{hasSearch}: {hasSearch: boolean},
) {
await expect(page.locator('#error-action-home-btn')).toBeVisible();
await expect(page.locator('#error-action-report')).toBeVisible();

if (hasSearch) {
await expect(page.locator('#error-action-search-btn')).toBeVisible();
} else {
await expect(page.locator('#error-action-search-btn')).toHaveCount(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {expect, fixture, html} from '@open-wc/testing';
import '../webstatus-not-found-error-page.js';
import {WebstatusNotFoundErrorPage} from '../webstatus-notfound-error-page.js';
import {Task} from '@lit/task';
import {APIClient} from '../../contexts/api-client-context.js';

type SimilarFeature = {name: string; url: string};

const GITHUB_REPO_ISSUE_LINK = 'https://github.com/example/repo/issues';

describe('webstatus-not-found-error-page', () => {
it('renders the correct error message when featureId is missing', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(
html`<webstatus-not-found-error-page></webstatus-not-found-error-page>`,
);

expect(
component.shadowRoot
?.querySelector('#error-status-code')
?.textContent?.trim(),
).to.equal('404');

expect(
component.shadowRoot
?.querySelector('#error-headline')
?.textContent?.trim(),
).to.equal('Page not found');

expect(
component.shadowRoot
?.querySelector('#error-detailed-message .error-message')
?.textContent?.trim(),
).to.equal("We couldn't find the page you're looking for.");
});

it('renders the correct error message when featureId is provided', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page
.location=${{search: '?q=test-feature'}}
></webstatus-not-found-error-page>
`);

expect(
component.shadowRoot?.querySelector('#error-detailed-message')
?.textContent,
).to.include('We could not find Feature ID: test-feature');
});

it('displays "Loading similar features..." when the API request is pending', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page
.location=${{search: '?q=test-feature'}}
></webstatus-not-found-error-page>
`);

// Override the _similarResults with a fake pending Task
component._similarResults = new Task(component, {
args: () => [undefined as unknown as APIClient, 'test-feature'], // no-op args
task: async () => {
return new Promise(() => {}); // never resolves = stays pending
},
});

// Trigger the task manually
component._similarResults.run();
await component.updateComplete;

const loadingMessage =
component.shadowRoot?.querySelector('.loading-message');
expect(loadingMessage).to.exist;
expect(loadingMessage?.textContent?.trim()).to.equal(
'Loading similar features...',
);
});

it('renders similar features when API returns results', async () => {
const mockData = [
{name: 'Feature One', url: '/features/dignissimos44'},
{name: 'Feature Two', url: '/features/fugiat37'},
];

const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page
.location=${{search: '?q=g'}}
></webstatus-not-found-error-page>
`);

// Patch _similarResults manually
component._similarResults = new Task<[APIClient, string], SimilarFeature[]>(
component,
{
args: () => [undefined as unknown as APIClient, 'g'],
task: async () => mockData,
},
);

component._similarResults.run(); // force task to execute
await component.updateComplete;

const featureList =
component.shadowRoot?.querySelectorAll('.feature-list li');
expect(featureList?.length).to.equal(2);
expect(featureList?.[0]?.textContent?.trim()).to.equal('Feature One');
expect(featureList?.[1]?.textContent?.trim()).to.equal('Feature Two');
});

it('renders all three buttons when featureId and similar results exist', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page
.location=${{search: '?q=g'}}
></webstatus-not-found-error-page>
`);

expect(component.shadowRoot?.querySelector('#error-action-search-btn')).to
.exist;
expect(component.shadowRoot?.querySelector('#error-action-home-btn')).to
.exist;
expect(component.shadowRoot?.querySelector('#error-action-report')).to
.exist;
});

it('renders only two buttons when featureId does not exist', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page
.location=${{search: ''}}
></webstatus-not-found-error-page>
`);

expect(component.shadowRoot?.querySelector('#error-action-search-btn')).to
.not.exist;
expect(component.shadowRoot?.querySelector('#error-action-home-btn')).to
.exist;
expect(component.shadowRoot?.querySelector('#error-action-report')).to
.exist;
});

it('search button contains the correct query parameter', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page
.location=${{search: '?q=correct-query'}}
></webstatus-not-found-error-page>
`);

const searchButton = component.shadowRoot?.querySelector(
'#error-action-search-btn',
);
expect(searchButton?.getAttribute('href')).to.equal('/?q=correct-query');
});

it('report issue button links to GitHub', async () => {
const component = await fixture<WebstatusNotFoundErrorPage>(html`
<webstatus-not-found-error-page></webstatus-not-found-error-page>
`);

const reportButton = component.shadowRoot?.querySelector(
'#error-action-report',
);
expect(reportButton?.getAttribute('href')).to.equal(GITHUB_REPO_ISSUE_LINK);
});
});
26 changes: 15 additions & 11 deletions frontend/src/static/js/components/webstatus-feature-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ import {
} from './webstatus-overview-cells.js';

import './webstatus-gchart';
import {NotFoundError} from '../api/errors.js';
import {BaseChartsPage} from './webstatus-base-charts-page.js';

import './webstatus-feature-wpt-progress-chart-panel.js';
import './webstatus-feature-usage-chart-panel.js';
import {DataFetchedEvent} from './webstatus-line-chart-panel.js';
import {NotFoundError} from '../api/errors.js';
// CanIUseData is a slimmed down interface of the data returned from the API.
interface CanIUseData {
items?: {
Expand Down Expand Up @@ -219,6 +219,19 @@ export class FeaturePage extends BaseChartsPage {
}
return Promise.reject('api client and/or featureId not set');
},
onError: async error => {
if (error instanceof NotFoundError) {
const queryParam = this.featureId ? `?q=${this.featureId}` : '';

// TODO: cannot use navigateToUrl because it creates a
// circular dependency.
// For now use the window href and revisit when navigateToUrl
// is move to another location.
window.location.href = `/errors-404/feature-not-found${queryParam}`;
} else {
console.error('Unexpected error in _loadingTask:', error);
}
},
});

this._loadingMetadataTask = new Task(this, {
Expand All @@ -242,16 +255,7 @@ export class FeaturePage extends BaseChartsPage {
return html`
${this._loadingTask?.render({
complete: () => this.renderWhenComplete(),
error: error => {
if (error instanceof NotFoundError) {
// TODO: cannot use navigateToUrl because it creates a
// circular dependency.
// For now use the window href and revisit when navigateToUrl
// is move to another location.
window.location.href = '/errors-404/feature-not-found';
}
return this.renderWhenError();
},
error: () => this.renderWhenError(),
initial: () => this.renderWhenInitial(),
pending: () => this.renderWhenPending(),
})}
Expand Down
Loading
Loading