Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/app/components/home-container/home-container.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
SPDX-License-Identifier: AGPL-3.0-only
-->

<acc-home-page [state]="state()" (reload)="reload()" />
<acc-home-page [state]="state()" />
48 changes: 6 additions & 42 deletions src/app/components/home-container/home-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,50 +71,14 @@ describe("HomeContainer", () => {
expect(TestBed.inject(Router).url).toBe("/login");
});

describe("when another error occurs", () => {
let fixture: ComponentFixture<HomeContainer>;
let page: HomePage;

beforeEach(async () => {
consoleApi.myOrganizationResult = err({ kind: "unknown" });
fixture = await render();
page = fixture.debugElement.query(By.directive(HomePage)).injector.get(HomePage);
});

function pageState(): HomePageState {
fixture.detectChanges();
return page.state();
}

function clickReload(): void {
const button: HTMLButtonElement = fixture.nativeElement.querySelector("button");
button.click();
}

it("should show an error", () => {
expect(pageState()).toBe(HomePageState.LoadingFailed);
});

describe("when reloading", () => {
it("should show progress for the minimum duration even if reloading fails quickly", async () => {
clickReload();

await vi.advanceTimersByTimeAsync(DELAY_MS - 1);
expect(pageState()).toBe(HomePageState.Reloading);

await vi.advanceTimersByTimeAsync(1);
expect(pageState()).toBe(HomePageState.LoadingFailed);
});

it("should redirect to the organization dashboard when reloading succeeds", async () => {
consoleApi.myOrganizationResult = ok({ organizationId: ORGANIZATION_ID });
it("should show an error when any other error occurs", async () => {
consoleApi.myOrganizationResult = err({ kind: "unknown" });

clickReload();
await vi.advanceTimersByTimeAsync(DELAY_MS);
const fixture = await render();

expect(TestBed.inject(Router).url).toBe(`/orgs/${ORGANIZATION_ID}/dashboard`);
});
});
fixture.detectChanges();
const page = fixture.debugElement.query(By.directive(HomePage)).injector.get(HomePage);
expect(page.state()).toBe(HomePageState.LoadingFailed);
});
});

Expand Down
20 changes: 4 additions & 16 deletions src/app/components/home-container/home-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,15 @@ export class HomeContainer {
protected readonly state = signal(HomePageState.Loading);

constructor() {
this.redirect();
}

/**
* Reloads the page, trying to determine where to redirect to after previously failing to do so.
*/
protected reload(): void {
this.state.set(HomePageState.Reloading);
this.redirect();
// Run as a pending task so that the app isn't considered stable until we've redirected or
// displayed an error
this.pendingTasks.run(() => this.redirect());
}

/**
* Determines where to send the user and redirects them there, displaying an error on failure.
*/
private redirect(): void {
// Run as a pending task so that the app isn't considered stable until we've redirected or
// displayed an error
this.pendingTasks.run(() => this.tryRedirect());
}

private async tryRedirect(): Promise<void> {
private async redirect(): Promise<void> {
const result = await withDelay(this.consoleApi.getMyOrganization());

if (result.ok) {
Expand Down
17 changes: 6 additions & 11 deletions src/app/components/home-page/home-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,18 @@
@case (HomePageState.Loading) {
<mat-progress-spinner mode="indeterminate" aria-label="Loading" />
}
@case (HomePageState.LoadingFailed)
@case (HomePageState.Reloading) {
@let reloading = currentState === HomePageState.Reloading;
@case (HomePageState.LoadingFailed) {
<div class="flex max-w-sm flex-col items-center gap-6 text-center">
<div class="flex flex-col gap-2" role="alert">
<h1 class="[font:var(--mat-sys-headline-small)]">Something went wrong</h1>
<p class="text-(--mat-sys-on-surface-variant)">
We couldn't load the console. Please try again.
We couldn't load the console. This isn't caused by anything you did. Please
try again in a few minutes, or check out our documentation in the meantime.
</p>
</div>
<acc-loading-button
appearance="filled"
[loading]="reloading"
loadingLabel="Trying again"
(press)="reload.emit()">
Try again
</acc-loading-button>
<a matButton="filled" href="https://accrescent.app/docs/" target="_blank">
Read the docs
</a>
</div>
}
@default never;
Expand Down
53 changes: 2 additions & 51 deletions src/app/components/home-page/home-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,23 @@
//
// SPDX-License-Identifier: AGPL-3.0-only

import { inputBinding, outputBinding, signal } from "@angular/core";
import { inputBinding, signal } from "@angular/core";
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { HomePage, HomePageState } from "./home-page";

describe("HomePage", () => {
let reloadCount: number;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomePage],
}).compileComponents();

reloadCount = 0;
});

/**
* Creates a {@link HomePage} in the given state and waits for it to render.
*/
async function render(state: HomePageState): Promise<ComponentFixture<HomePage>> {
const fixture = TestBed.createComponent(HomePage, {
bindings: [
inputBinding("state", signal(state)),
outputBinding("reload", () => reloadCount++),
],
bindings: [inputBinding("state", signal(state))],
});
await fixture.whenStable();
return fixture;
Expand Down Expand Up @@ -54,47 +47,5 @@ describe("HomePage", () => {
const alert: HTMLElement = fixture.nativeElement.querySelector("[role=alert]");
expect(alert.textContent).toContain("Something went wrong");
});

it("should emit reload when the reload button is clicked", () => {
fixture.nativeElement.querySelector("button").click();

expect(reloadCount).toBe(1);
});
});

describe("when reloading", () => {
let fixture: ComponentFixture<HomePage>;

beforeEach(async () => {
fixture = await render(HomePageState.Reloading);
});

it("should show an error", () => {
const alert: HTMLElement = fixture.nativeElement.querySelector("[role=alert]");
expect(alert.textContent).toContain("Something went wrong");
});

it("should show a progress indicator in the reload button", () => {
const button: HTMLButtonElement = fixture.nativeElement.querySelector("button");
expect(button.querySelector("mat-progress-spinner")).not.toBeNull();
});

it("should disable the reload button but keep it focusable", () => {
const button: HTMLButtonElement = fixture.nativeElement.querySelector("button");

expect(button.disabled).toBe(false);
expect(button.getAttribute("aria-disabled")).toBe("true");
});

it("should give the reload button an accessible name describing the reload in progress", () => {
const button: HTMLButtonElement = fixture.nativeElement.querySelector("button");
expect(button.getAttribute("aria-label")).toBe("Trying again");
});

it("should not emit reload when the reload button is clicked", () => {
fixture.nativeElement.querySelector("button").click();

expect(reloadCount).toBe(0);
});
});
});
6 changes: 0 additions & 6 deletions src/app/components/home-page/home-page.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,3 @@ export const LoadingFailed: Story = {
state: HomePageState.LoadingFailed,
},
};

export const Reloading: Story = {
args: {
state: HomePageState.Reloading,
},
};
16 changes: 3 additions & 13 deletions src/app/components/home-page/home-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//
// SPDX-License-Identifier: AGPL-3.0-only

import { Component, input, output } from "@angular/core";
import { Component, input } from "@angular/core";
import { MatButton } from "@angular/material/button";
import { MatProgressSpinner } from "@angular/material/progress-spinner";
import { LoadingButton } from "../loading-button/loading-button";

/**
* The state of the {@link HomePage}.
Expand All @@ -19,18 +19,13 @@ export enum HomePageState {
* Determining where to send the user failed.
*/
LoadingFailed,

/**
* Determining where to send the user after a previous attempt failed.
*/
Reloading,
}

/**
* The page shown while determining where to send the user on entering the console.
*/
@Component({
imports: [LoadingButton, MatProgressSpinner],
imports: [MatButton, MatProgressSpinner],
selector: "acc-home-page",
templateUrl: "./home-page.html",
})
Expand All @@ -41,9 +36,4 @@ export class HomePage {
* The state of determining where to send the user.
*/
readonly state = input.required<HomePageState>();

/**
* Emits when the user clicks the reload button.
*/
readonly reload = output<void>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
SPDX-License-Identifier: AGPL-3.0-only
-->

<acc-org-dashboard-page class="block h-full" [state]="state()" (reload)="reload()" />
<acc-org-dashboard-page class="block h-full" [state]="state()" />
Original file line number Diff line number Diff line change
Expand Up @@ -105,46 +105,12 @@ describe("OrgDashboardContainer", () => {
expect(TestBed.inject(Router).url).toBe("/login");
});

describe("when another error occurs", () => {
let fixture: ComponentFixture<OrgDashboardContainer>;
it("should show an error when any other error occurs", async () => {
consoleApi.organizationResult = err({ kind: "unknown" });

beforeEach(async () => {
consoleApi.organizationResult = err({ kind: "unknown" });
fixture = await render();
});

function clickReload(): void {
const button: HTMLButtonElement = fixture.nativeElement.querySelector("button");
button.click();
}

it("should show an error", () => {
expect(pageState(fixture)).toEqual({ kind: "loadingFailed" });
});

describe("when reloading", () => {
it("should show progress for the minimum duration even if reloading fails quickly", async () => {
clickReload();

await vi.advanceTimersByTimeAsync(DELAY_MS - 1);
expect(pageState(fixture)).toEqual({ kind: "reloading" });

await vi.advanceTimersByTimeAsync(1);
expect(pageState(fixture)).toEqual({ kind: "loadingFailed" });
});

it("should show the organization when reloading succeeds", async () => {
consoleApi.organizationResult = ok({ organization: ORGANIZATION });

clickReload();
await vi.advanceTimersByTimeAsync(DELAY_MS);
const fixture = await render();

expect(pageState(fixture)).toEqual({
kind: "loaded",
organization: ORGANIZATION,
});
});
});
expect(pageState(fixture)).toEqual({ kind: "loadingFailed" });
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,8 @@ export class OrgDashboardContainer {
withDelay(this.consoleApi.getOrganization(params, abortSignal)),
});
protected readonly state = computed((): OrgDashboardPageState => {
switch (this.organization.status()) {
case "reloading":
return { kind: "reloading" };
case "error":
return { kind: "loadingFailed" };
if (this.organization.status() === "error") {
return { kind: "loadingFailed" };
}
return this.organization.hasValue()
? toPageState(this.organization.value())
Expand All @@ -61,10 +58,6 @@ export class OrgDashboardContainer {
}
});
}

protected reload(): void {
this.organization.reload();
}
}

function toPageState(
Expand Down
17 changes: 7 additions & 10 deletions src/app/components/org-dashboard-page/org-dashboard-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,19 @@
<mat-progress-spinner mode="indeterminate" aria-label="Loading" />
</div>
}
@case ("loadingFailed")
@case ("reloading") {
@case ("loadingFailed") {
<div class="m-auto flex max-w-sm flex-col items-center gap-6 text-center">
<div class="flex flex-col gap-2" role="alert">
<h1 class="[font:var(--mat-sys-headline-small)]">Something went wrong</h1>
<p class="text-(--mat-sys-on-surface-variant)">
We couldn't load this organization. Please try again.
We couldn't load your organization. This isn't caused by anything you did.
Please try again in a few minutes, or check out our documentation in the
meantime.
</p>
</div>
<acc-loading-button
appearance="filled"
[loading]="currentState.kind === 'reloading'"
loadingLabel="Trying again"
(press)="reload.emit()">
Try again
</acc-loading-button>
<a matButton="filled" href="https://accrescent.app/docs/" target="_blank">
Read the docs
</a>
</div>
}
@case ("permissionDenied") {
Expand Down
Loading
Loading