Skip to content

Commit

Permalink
Add e2e test for canisters (dfinity#4549)
Browse files Browse the repository at this point in the history
# Motivation

This is prompted by the change to read transactions from the index
canister so we want to make sure we still render transaction such as
"Create Canister" and "Top-up Canister" correctly.
But we did not yet have any end-to-end test for canisters so I added
more than just the minimum to check transactions.

# Changes

1. Add an end-to-end test which creates a canister, renames a canister
and tops-up a canister.
2. Add missing test IDs.
3. Fix one typo in an existing test ID.
4. Add necessary page objects.
5. Factor out a function to navigate to the NNS main account and reuse
it.
6. Change `typeText` on the Playwright page object to use `fill()`
instead of `type()`. Fill replaces existing text, which we need because
when renaming the canister, the input field is prefilled with the
existing name. Since nothing broke, I assume we never relied on
`typeText()` leaving the existing text there.

# Tests

added

# Todos

- [ ] Add entry to changelog (if necessary).
not necessary
  • Loading branch information
dskloetd authored Feb 23, 2024
1 parent edf76d7 commit e1a252b
Show file tree
Hide file tree
Showing 19 changed files with 374 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
$: ({ canisterId, validName } = mapCanisterDetails(canister));
</script>

<div class={`title-block ${titleTag}`} data-tid="canister-card-title-compoment">
<div class={`title-block ${titleTag}`} data-tid="canister-card-title-component">
<svelte:element this={titleTag} class="title value"
><span>{validName ? canister.name : canisterId}</span>
{#if !validName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<form on:submit|preventDefault={selectAmount} data-tid="select-cycles-screen">
<div class="inputs">
<Input
testId="select-cycles-icp-input"
placeholderLabelKey="core.icp"
inputType="icp"
name="icp-amount"
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/components/ui/Input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { translate } from "$lib/utils/i18n.utils";
import { Input } from "@dfinity/gix-components";
export let testId: string = "input-ui-element";
export let name: string;
export let inputType: "icp" | "number" | "text" = "number";
export let required = true;
Expand All @@ -20,7 +21,7 @@
</script>

<Input
testId="input-ui-element"
{testId}
{inputType}
{required}
{spellcheck}
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/lib/modals/canisters/AddCyclesModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,13 @@
};
</script>

<WizardModal {steps} bind:currentStep bind:this={modal} on:nnsClose>
<WizardModal
testId="add-cycles-modal-component"
{steps}
bind:currentStep
bind:this={modal}
on:nnsClose
>
<svelte:fragment slot="title"
><span data-tid="top-up-canister-modal-title"
>{currentStep?.title ?? $i18n.accounts.select_source}</span
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/lib/modals/canisters/CreateCanisterModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@
};
</script>

<WizardModal {steps} bind:currentStep bind:this={modal} on:nnsClose>
<WizardModal
testId="create-canister-modal-component"
{steps}
bind:currentStep
bind:this={modal}
on:nnsClose
>
<svelte:fragment slot="title"
><span data-tid="create-canister-modal-title"
>{currentStep?.title ?? $i18n.canisters.add_canister}</span
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/pages/CanisterDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@

<Footer columns={1}>
<button
data-tid="add-cycles-button"
class="primary"
on:click={openModal}
disabled={canisterInfo === undefined || $busy}
Expand Down
89 changes: 46 additions & 43 deletions frontend/src/lib/pages/Canisters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { listCanisters } from "$lib/services/canisters.services";
import { canistersStore } from "$lib/stores/canisters.store";
import { AppPath } from "$lib/constants/routes.constants";
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import SkeletonCard from "$lib/components/ui/SkeletonCard.svelte";
import CanisterCard from "$lib/components/canisters/CanisterCard.svelte";
import type { CanisterId } from "$lib/canisters/nns-dapp/nns-dapp.types";
Expand Down Expand Up @@ -64,52 +65,54 @@
const closeModal = () => (modal = undefined);
</script>

<main>
<Summary displayUniverse={false}>
<PrincipalText slot="details" inline />
</Summary>

<div class="card-grid">
{#each $canistersStore.canisters ?? [] as canister (canister.canister_id)}
<CanisterCard
ariaLabel={$i18n.canisters.aria_label_canister_card}
href={buildCanisterDetailsHref(canister.canister_id)}
{canister}
/>
{/each}

{#if loading}
<SkeletonCard />
<SkeletonCard />
<TestIdWrapper testId="canisters-component">
<main>
<Summary displayUniverse={false}>
<PrincipalText slot="details" inline />
</Summary>

<div class="card-grid">
{#each $canistersStore.canisters ?? [] as canister (canister.canister_id)}
<CanisterCard
ariaLabel={$i18n.canisters.aria_label_canister_card}
href={buildCanisterDetailsHref(canister.canister_id)}
{canister}
/>
{/each}

{#if loading}
<SkeletonCard />
<SkeletonCard />
{/if}
</div>

{#if noCanisters}
<p class="description empty">{$i18n.canisters.text}</p>
{/if}
</div>
</main>

{#if noCanisters}
<p class="description empty">{$i18n.canisters.text}</p>
{#if modal === "CreateCanister"}
<CreateCanisterModal on:nnsClose={closeModal} />
{/if}
</main>

{#if modal === "CreateCanister"}
<CreateCanisterModal on:nnsClose={closeModal} />
{/if}
{#if modal === "LinkCanister"}
<LinkCanisterModal on:nnsClose={closeModal} />
{/if}

<Footer>
<button
data-tid="create-canister-button"
class="primary"
on:click={() => openModal("CreateCanister")}
>{$i18n.canisters.create_canister}</button
>
<button
data-tid="link-canister-button"
class="secondary"
on:click={() => openModal("LinkCanister")}
>{$i18n.canisters.link_canister}</button
>
</Footer>
{#if modal === "LinkCanister"}
<LinkCanisterModal on:nnsClose={closeModal} />
{/if}

<Footer>
<button
data-tid="create-canister-button"
class="primary"
on:click={() => openModal("CreateCanister")}
>{$i18n.canisters.create_canister}</button
>
<button
data-tid="link-canister-button"
class="secondary"
on:click={() => openModal("LinkCanister")}
>{$i18n.canisters.link_canister}</button
>
</Footer>
</TestIdWrapper>

<style lang="scss">
@use "@dfinity/gix-components/dist/styles/mixins/media";
Expand Down
61 changes: 61 additions & 0 deletions frontend/src/tests/e2e/canisters.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { AppPo } from "$tests/page-objects/App.page-object";
import { PlaywrightPageObjectElement } from "$tests/page-objects/playwright.page-object";
import { signInWithNewUser, step } from "$tests/utils/e2e.test-utils";
import { expect, test } from "@playwright/test";

test("Test canisters", async ({ page, context }) => {
await page.goto("/");
await expect(page).toHaveTitle("My Tokens / NNS Dapp");
await signInWithNewUser({ page, context });

const pageElement = PlaywrightPageObjectElement.fromPage(page);
const appPo = new AppPo(pageElement);

step("Get some ICP");
await appPo.getIcpTokens(10);

step("Create a canister");
const canisterName = "MyCanister";
await appPo.goToCanisters();
const canistersPo = appPo.getCanistersPo();
await canistersPo.createCanister({
name: canisterName,
icpAmount: "1",
});

step("Rename canister");
const canisterCards = await canistersPo.getCanisterCardPos();
expect(canisterCards).toHaveLength(1);
const canisterCard = canisterCards[0];
expect(await canisterCard.getCanisterName()).toBe(canisterName);
await canisterCard.click();

const newCanisterName = "MyCanister2";
const canisterDetail = appPo.getCanisterDetailPo();
await canisterDetail.clickRename();
await canisterDetail.renameCanister(newCanisterName);

step("Top up canister");
await canisterDetail.addCycles({ icpAmount: "2" });

step("Verify name");
await appPo.goBack();
expect(await canisterCard.getCanisterName()).toBe(newCanisterName);

step("Check transaction descriptions");
await appPo.goToNnsMainAccountWallet();
const transactionList = appPo
.getWalletPo()
.getNnsWalletPo()
.getTransactionListPo();
await transactionList.waitForLoaded();
const transactions = await transactionList.getTransactionCardPos();
expect(await Promise.all(transactions.map((tx) => tx.getHeadline()))).toEqual(
["Top-up Canister", "Create Canister", "Received"]
);
expect(await Promise.all(transactions.map((tx) => tx.getAmount()))).toEqual([
"-2.0001",
"-1.0001",
"+10.00",
]);
});
13 changes: 1 addition & 12 deletions frontend/src/tests/e2e/merge-neurons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,7 @@ test("Test merge neurons", async ({ page, context }) => {
// Reload the page in case we would only know the neuron because it was still
// in the store from before.
await page.goto("/");
await appPo
.getTokensPo()
.getTokensPagePo()
.getTokensTable()
.getRowByName("Internet Computer")
.click();
await appPo
.getAccountsPo()
.getNnsAccountsPo()
.getTokensTablePo()
.getRowByName("Main")
.click();
await appPo.goToNnsMainAccountWallet();
const transactionList = appPo
.getWalletPo()
.getNnsWalletPo()
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/tests/page-objects/AddCyclesModal.page-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ConfirmCyclesCanisterPo } from "$tests/page-objects/ConfirmCyclesCanister.page-object";
import { ModalPo } from "$tests/page-objects/Modal.page-object";
import { SelectCyclesCanisterPo } from "$tests/page-objects/SelectCyclesCanister.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";

export class AddCyclesModalPo extends ModalPo {
private static readonly TID = "add-cycles-modal-component";

static under(element: PageObjectElement): AddCyclesModalPo {
return new AddCyclesModalPo(element.byTestId(AddCyclesModalPo.TID));
}

getSelectCyclesCanisterPo(): SelectCyclesCanisterPo {
return SelectCyclesCanisterPo.under(this.root);
}

getConfirmCyclesCanisterPo(): ConfirmCyclesCanisterPo {
return ConfirmCyclesCanisterPo.under(this.root);
}

enterIcpAmount(amount: string): Promise<void> {
return this.getSelectCyclesCanisterPo().enterIcpAmount(amount);
}

clickReview(): Promise<void> {
return this.getSelectCyclesCanisterPo().clickSubmit();
}

clickConfirm(): Promise<void> {
return this.getConfirmCyclesCanisterPo().clickConfirm();
}

async addCycles({ icpAmount }: { icpAmount: string }): Promise<void> {
await this.enterIcpAmount(icpAmount);
await this.clickReview();
await this.clickConfirm();
}
}
31 changes: 31 additions & 0 deletions frontend/src/tests/page-objects/App.page-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { AccountsPo } from "$tests/page-objects/Accounts.page-object";
import { BackdropPo } from "$tests/page-objects/Backdrop.page-object";
import { BusyScreenPo } from "$tests/page-objects/BusyScreen.page-object";
import type { ButtonPo } from "$tests/page-objects/Button.page-object";
import { CanisterDetailPo } from "$tests/page-objects/CanisterDetail.page-object";
import { CanistersPo } from "$tests/page-objects/Canisters.page-object";
import { LaunchpadPo } from "$tests/page-objects/Launchpad.page-object";
import { MenuItemsPo } from "$tests/page-objects/MenuItems.page-object";
import { NeuronDetailPo } from "$tests/page-objects/NeuronDetail.page-object";
Expand Down Expand Up @@ -67,6 +69,14 @@ export class AppPo extends BasePageObject {
return ProjectDetailPo.under(this.root);
}

getCanistersPo(): CanistersPo {
return CanistersPo.under(this.root);
}

getCanisterDetailPo(): CanisterDetailPo {
return CanisterDetailPo.under(this.root);
}

getMenuItemsPo(): MenuItemsPo {
return MenuItemsPo.under(this.root);
}
Expand Down Expand Up @@ -112,6 +122,20 @@ export class AppPo extends BasePageObject {
await this.getBackdropPo().waitForAbsent();
}

async goToNnsMainAccountWallet(): Promise<void> {
await this.goToAccounts();
await this.getTokensPo()
.getTokensPagePo()
.getTokensTable()
.getRowByName("Internet Computer")
.click();
await this.getAccountsPo()
.getNnsAccountsPo()
.getTokensTablePo()
.getRowByName("Main")
.click();
}

async goToNeurons(): Promise<void> {
await this.openMenu();
await this.getMenuItemsPo().clickNeuronStaking();
Expand Down Expand Up @@ -142,6 +166,13 @@ export class AppPo extends BasePageObject {
await this.getBackdropPo().waitForAbsent();
}

async goToCanisters(): Promise<void> {
await this.openMenu();
await this.getMenuItemsPo().clickCanisters();
// Menu closes automatically.
await this.getBackdropPo().waitForAbsent();
}

async getSnsTokens(params: { amount: number; name: string }): Promise<void> {
await this.openMenu();
await this.getMenuItemsPo().getGetTokensPo().getSnsTokens(params);
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/tests/page-objects/CanisterCard.page-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CardPo } from "$tests/page-objects/Card.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";

export class CanisterCardPo extends CardPo {
private static readonly TID = "canister-card";

static async allUnder(element: PageObjectElement): Promise<CanisterCardPo[]> {
return Array.from(await element.allByTestId(CanisterCardPo.TID)).map(
(el) => new CanisterCardPo(el)
);
}

static under(element: PageObjectElement): CanisterCardPo {
return new CanisterCardPo(element.byTestId(CanisterCardPo.TID));
}

async getCanisterName(): Promise<string> {
return (await this.getText("canister-card-title-component")).trim();
}
}
Loading

0 comments on commit e1a252b

Please sign in to comment.