Compare commits
42 Commits
e20cd2aac1
...
b8181f6ae3
Author | SHA1 | Date |
---|---|---|
Trent Larson | b8181f6ae3 | 4 months ago |
Jose Olarte III | bcc0fac0a8 | 4 months ago |
Trent Larson | f724476ed6 | 4 months ago |
Jose Olarte III | 6ca5bf754b | 4 months ago |
Jose Olarte III | 25eaff62d8 | 4 months ago |
Jose Olarte III | dd1532e2f4 | 4 months ago |
Jose Olarte III | 7033d259e1 | 4 months ago |
Jose Olarte III | aae2e62177 | 4 months ago |
Jose Olarte III | 9a9c2b1813 | 4 months ago |
Jose Olarte III | ee75576cda | 4 months ago |
Jose Olarte III | 88efa36542 | 4 months ago |
Trent Larson | fe1cd32be1 | 4 months ago |
Trent Larson | c8f0f2c2b1 | 4 months ago |
Trent Larson | 7aaf981b71 | 4 months ago |
Trent Larson | ca8da9fd5e | 4 months ago |
Trent Larson | ff3d397150 | 4 months ago |
Trent Larson | 052d5c5bd1 | 4 months ago |
Trent Larson | 9213ad1f4a | 4 months ago |
Trent Larson | 98f4665465 | 4 months ago |
Jose Olarte III | c5f5e81f2c | 4 months ago |
Jose Olarte III | 26f6eb66fe | 4 months ago |
Jose Olarte III | 28dd602d9f | 4 months ago |
Jose Olarte III | fb0a64c0ab | 4 months ago |
Jose Olarte III | 88f41b885c | 4 months ago |
Trent Larson | bbf621fb18 | 4 months ago |
Jose Olarte III | a0adc1517c | 4 months ago |
Jose Olarte III | 812c8a418e | 4 months ago |
Jose Olarte III | 2f0326f182 | 4 months ago |
Trent Larson | 1fbd1da87d | 4 months ago |
Trent Larson | d96770a351 | 4 months ago |
Trent Larson | 8d684f1b29 | 4 months ago |
Trent Larson | 6272b3045b | 4 months ago |
Trent Larson | 375d6ddbe2 | 4 months ago |
Trent Larson | f497c53294 | 4 months ago |
Trent Larson | cb8aeeac1b | 4 months ago |
Trent Larson | 6191a4893f | 4 months ago |
Trent Larson | 791a35d97c | 4 months ago |
Trent Larson | 5647c4627f | 4 months ago |
Jose Olarte III | 7dfc377610 | 4 months ago |
Jose Olarte III | 11a3e981a6 | 4 months ago |
Trent Larson | cd04f35224 | 4 months ago |
Jose Olarte III | 59f97ffc28 | 4 months ago |
40 changed files with 1024 additions and 951 deletions
@ -1,4 +1,4 @@ |
|||
# Only the variables that start with VITE_ are seen in the application process.env in Vue. |
|||
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. |
|||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H |
|||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch |
|||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app |
|||
|
@ -0,0 +1,98 @@ |
|||
import { defineConfig, devices } from '@playwright/test'; |
|||
|
|||
/** |
|||
* Read environment variables from file. |
|||
* https://github.com/motdotla/dotenv
|
|||
*/ |
|||
// import dotenv from 'dotenv';
|
|||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
|||
|
|||
/** |
|||
* See https://playwright.dev/docs/test-configuration.
|
|||
*/ |
|||
export default defineConfig({ |
|||
testDir: './test-playwright', |
|||
/* Run tests in files in parallel */ |
|||
fullyParallel: true, |
|||
/* Fail the build on CI if you accidentally left test.only in the source code. */ |
|||
forbidOnly: !!process.env.CI, |
|||
/* Retry on CI only */ |
|||
retries: process.env.CI ? 2 : 0, |
|||
/* Opt out of parallel tests on CI. */ |
|||
workers: process.env.CI ? 1 : undefined, |
|||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ |
|||
reporter: 'html', |
|||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ |
|||
use: { |
|||
/* Base URL to use in actions like `await page.goto('/')`. */ |
|||
baseURL: 'http://localhost:8080', |
|||
|
|||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ |
|||
trace: 'on-first-retry', |
|||
}, |
|||
|
|||
/* Configure projects for major browsers */ |
|||
projects: [ |
|||
{ |
|||
name: 'chromium', |
|||
use: { |
|||
...devices['Desktop Chrome'], |
|||
permissions: ["clipboard-read"], |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
name: 'firefox', |
|||
use: { ...devices['Desktop Firefox'] }, |
|||
}, |
|||
|
|||
{ |
|||
name: 'webkit', |
|||
use: { ...devices['Desktop Safari'] }, |
|||
}, |
|||
|
|||
/* Test against mobile viewports. */ |
|||
{ |
|||
name: 'Mobile Chrome', |
|||
use: { ...devices['Pixel 5'] }, |
|||
}, |
|||
{ |
|||
name: 'Mobile Safari', |
|||
use: { ...devices['iPhone 12'] }, |
|||
}, |
|||
|
|||
/* Test against branded browsers. */ |
|||
// {
|
|||
// name: 'Microsoft Edge',
|
|||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|||
// },
|
|||
{ |
|||
name: 'Google Chrome', |
|||
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, |
|||
}, |
|||
], |
|||
|
|||
/* Configure global timeout */ |
|||
// the image upload will often not succeed at 5 seconds
|
|||
//timeout: 7000,
|
|||
|
|||
/* Run your local dev server before starting the tests */ |
|||
/** |
|||
* This could be an array of servers, meaning we could start the Endorser server as well: |
|||
* { |
|||
* command: "cd ../endorser-ch; NODE_ENV=test-local npm run dev", |
|||
* url: 'http://localhost:3000', |
|||
* reuseExistingServer: !process.env.CI, |
|||
* }, |
|||
* |
|||
* But if we do then the testInfo.config.webServer is null and the API-setting test 00 fails. |
|||
* It is worth considering a change such that Time Safari's default Endorer API server is NOT set |
|||
* in the user's settings so that it can be blanked out and the default is used. |
|||
*/ |
|||
webServer: { |
|||
command: |
|||
"VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run dev", |
|||
url: "http://localhost:8080", |
|||
reuseExistingServer: !process.env.CI, |
|||
}, |
|||
}); |
@ -0,0 +1,190 @@ |
|||
<template> |
|||
<QuickNav selected="Contacts"></QuickNav> |
|||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> |
|||
<!-- Back --> |
|||
<div class="text-lg text-center font-light relative px-7"> |
|||
<h1 |
|||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" |
|||
@click="$router.back()" |
|||
> |
|||
<fa icon="chevron-left" class="fa-fw"></fa> |
|||
</h1> |
|||
</div> |
|||
|
|||
<!-- Heading --> |
|||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> |
|||
Contact Import |
|||
</h1> |
|||
|
|||
<span> |
|||
Note that you will have to make them visible one-by-one in the list of |
|||
Contacts. |
|||
</span> |
|||
<div v-if="sameCount > 0"> |
|||
{{ sameCount }} contact{{ sameCount == 1 ? "" : "s" }} are the same as |
|||
existing contacts. |
|||
</div> |
|||
|
|||
<!-- Results List --> |
|||
<ul v-if="contactsImporting.length > 0" class="border-t border-slate-300"> |
|||
<li v-for="(contact, index) in contactsImporting" :key="contact.did"> |
|||
<div |
|||
v-if=" |
|||
!contactsExisting[contact.did] || |
|||
!R.isEmpty(contactDifferences[contact.did]) |
|||
" |
|||
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4" |
|||
> |
|||
<h2 class="text-base font-semibold"> |
|||
<input type="checkbox" v-model="contactsSelected[index]" /> |
|||
{{ contact.name || AppString.NO_CONTACT_NAME }} |
|||
- |
|||
<span v-if="contactsExisting[contact.did]" class="text-orange-500" |
|||
>Existing</span |
|||
> |
|||
<span v-else class="text-green-500">New</span> |
|||
</h2> |
|||
<div class="text-sm truncate"> |
|||
{{ contact.did }} |
|||
</div> |
|||
<div v-if="contactDifferences[contact.did]"> |
|||
<div> |
|||
<div class="grid grid-cols-3 gap-2"> |
|||
<div class="font-bold">Field</div> |
|||
<div class="font-bold">Old Value</div> |
|||
<div class="font-bold">New Value</div> |
|||
</div> |
|||
<div |
|||
v-for="(value, contactField) in contactDifferences[contact.did]" |
|||
:key="contactField" |
|||
class="grid grid-cols-3 border" |
|||
> |
|||
<div class="border p-1">{{ contactField }}</div> |
|||
<div class="border p-1">{{ value.old }}</div> |
|||
<div class="border p-1">{{ value.new }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</li> |
|||
<fa icon="spinner" v-if="importing" class="animate-spin" /> |
|||
<button |
|||
v-else |
|||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded" |
|||
@click="importContacts" |
|||
> |
|||
Import Selected Contacts |
|||
</button> |
|||
</ul> |
|||
<p v-else>There are no contacts to import.</p> |
|||
</section> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import * as R from "ramda"; |
|||
import { Component, Vue } from "vue-facing-decorator"; |
|||
import { Router } from "vue-router"; |
|||
|
|||
import { AppString, NotificationIface } from "@/constants/app"; |
|||
import { db } from "@/db/index"; |
|||
import { Contact } from "@/db/tables/contacts"; |
|||
import * as libsUtil from "@/libs/util"; |
|||
import QuickNav from "@/components/QuickNav.vue"; |
|||
import EntityIcon from "@/components/EntityIcon.vue"; |
|||
import OfferDialog from "@/components/OfferDialog.vue"; |
|||
|
|||
@Component({ |
|||
components: { EntityIcon, OfferDialog, QuickNav }, |
|||
}) |
|||
export default class ContactImportView extends Vue { |
|||
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|||
|
|||
AppString = AppString; |
|||
libsUtil = libsUtil; |
|||
R = R; |
|||
|
|||
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID |
|||
contactsImporting: Array<Contact> = []; // contacts from the import |
|||
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected |
|||
contactDifferences: Record< |
|||
string, |
|||
Record<string, { new: string; old: string }> |
|||
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key |
|||
importing = false; |
|||
sameCount = 0; |
|||
|
|||
async created() { |
|||
// Retrieve the imported contacts from the query parameter |
|||
const importedContacts = |
|||
((this.$route as Router).query["contacts"] as string) || "[]"; |
|||
this.contactsImporting = JSON.parse(importedContacts); |
|||
this.contactsSelected = new Array(this.contactsImporting.length).fill( |
|||
false, |
|||
); |
|||
|
|||
await db.open(); |
|||
const baseContacts = await db.contacts.toArray(); |
|||
// set the existing contacts, keyed by DID, if they exist in contactsImporting |
|||
for (let i = 0; i < this.contactsImporting.length; i++) { |
|||
const contactIn = this.contactsImporting[i]; |
|||
const existingContact = baseContacts.find( |
|||
(contact) => contact.did === contactIn.did, |
|||
); |
|||
if (existingContact) { |
|||
this.contactsExisting[contactIn.did] = existingContact; |
|||
|
|||
const differences: Record<string, { new: string; old: string }> = {}; |
|||
Object.keys(contactIn).forEach((key) => { |
|||
if (contactIn[key] !== existingContact[key]) { |
|||
differences[key] = { |
|||
old: existingContact[key], |
|||
new: contactIn[key], |
|||
}; |
|||
} |
|||
}); |
|||
this.contactDifferences[contactIn.did] = differences; |
|||
if (R.isEmpty(differences)) { |
|||
this.sameCount++; |
|||
} |
|||
} else { |
|||
// automatically import new data |
|||
this.contactsSelected[i] = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
async importContacts() { |
|||
this.importing = true; |
|||
let importedCount = 0, |
|||
updatedCount = 0; |
|||
for (let i = 0; i < this.contactsImporting.length; i++) { |
|||
if (this.contactsSelected[i]) { |
|||
const contact = this.contactsImporting[i]; |
|||
const existingContact = this.contactsExisting[contact.did]; |
|||
if (existingContact) { |
|||
await db.contacts.update(contact.did, contact); |
|||
updatedCount++; |
|||
} else { |
|||
// without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned. |
|||
await db.contacts.add(R.clone(contact)); |
|||
importedCount++; |
|||
} |
|||
} |
|||
} |
|||
this.importing = false; |
|||
|
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Import Success", |
|||
text: |
|||
`${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` + |
|||
(updatedCount ? ` ${updatedCount} updated.` : ""), |
|||
}, |
|||
3000, |
|||
); |
|||
(this.$router as Router).push({ name: "contacts" }); |
|||
} |
|||
} |
|||
</script> |
@ -1,12 +0,0 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Check activity feed', async ({ page }) => { |
|||
// Load app homepage
|
|||
await page.goto('./'); |
|||
|
|||
// Check that initial 10 activities have been loaded
|
|||
await page.locator('li:nth-child(10)'); |
|||
|
|||
// Scroll down a bit to trigger loading additional activities
|
|||
await page.locator('li:nth-child(50)').scrollIntoViewIfNeeded(); |
|||
}); |
@ -0,0 +1,81 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Confirm usage of test API', async ({ page }, testInfo) => { |
|||
// Load account view
|
|||
await page.goto('./account'); |
|||
await page.getByRole('heading', { name: 'Advanced' }).click(); |
|||
|
|||
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
|||
const webServer = testInfo.config.webServer; |
|||
const endorserWords = webServer?.command.split(' '); |
|||
const ENDORSER_ENV_NAME = 'VITE_DEFAULT_ENDORSER_API_SERVER'; |
|||
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '=')); |
|||
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1); |
|||
|
|||
const endorserServer = endorserTermInConfig || 'https://test-api.endorser.ch'; |
|||
await expect(page.getByRole('textbox').nth(1)).toHaveValue(endorserServer); |
|||
}); |
|||
|
|||
test('Check activity feed', async ({ page }) => { |
|||
// Load app homepage
|
|||
await page.goto('./'); |
|||
|
|||
// Check that initial 10 activities have been loaded
|
|||
await page.locator('ul#listLatestActivity li:nth-child(10)'); |
|||
|
|||
// Scroll down a bit to trigger loading additional activities
|
|||
await page.locator('ul#listLatestActivity li:nth-child(50)').scrollIntoViewIfNeeded(); |
|||
}); |
|||
|
|||
test('Check discover results', async ({ page }) => { |
|||
// Load Discover view
|
|||
await page.goto('./discover'); |
|||
|
|||
// Check that initial 10 projects have been loaded
|
|||
await page.locator('ul#listDiscoverResults li.border-b:nth-child(10)'); |
|||
|
|||
// Scroll down a bit to trigger loading additional projects
|
|||
await page.locator('ul#listDiscoverResults li.border-b:nth-child(20)').scrollIntoViewIfNeeded(); |
|||
}); |
|||
|
|||
test('Check no-ID messaging in homepage', async ({ page }) => { |
|||
// Load app homepage
|
|||
await page.goto('./'); |
|||
|
|||
// Check 'someone must register you' notice
|
|||
await expect(page.getByText('To share, someone must register you.')).toBeVisible(); |
|||
}); |
|||
|
|||
test('Check no-ID messaging in account', async ({ page }) => { |
|||
// Load account view
|
|||
await page.goto('./account'); |
|||
|
|||
// Check 'someone must register you' notice
|
|||
await expect(page.locator('#noticeBeforeShare')).toBeVisible(); |
|||
|
|||
// Check 'a friend needs to register you' notice
|
|||
await expect(page.locator('#noticeBeforeAnnounce')).toBeVisible(); |
|||
|
|||
// Check that there is no ID
|
|||
await expect(page.locator('#sectionIdentityDetails code.truncate')).toBeEmpty(); |
|||
}); |
|||
|
|||
test('Check ID generation', async ({ page }) => { |
|||
// Load Account view
|
|||
await page.goto('./account'); |
|||
|
|||
// Check that ID is empty
|
|||
await expect(page.locator('#sectionIdentityDetails code.truncate')).toBeEmpty(); |
|||
|
|||
// Load homepage to trigger ID generation (?)
|
|||
await page.goto('./'); |
|||
|
|||
// Wait for activity feed to start loading, as a delay
|
|||
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible(); |
|||
|
|||
// Go back to Account view
|
|||
await page.goto('./account'); |
|||
|
|||
// Check that ID is now generated
|
|||
await expect(page.locator('#sectionIdentityDetails code.truncate')).toContainText('did:ethr:'); |
|||
}); |
@ -1,12 +0,0 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Check discover results', async ({ page }) => { |
|||
// Load Discover view
|
|||
await page.goto('./discover'); |
|||
|
|||
// Check that initial 10 projects have been loaded
|
|||
await page.locator('section#Content li.border-b:nth-child(10)'); |
|||
|
|||
// Scroll down a bit to trigger loading additional projects
|
|||
await page.locator('section#Content li.border-b:nth-child(20)').scrollIntoViewIfNeeded(); |
|||
}); |
@ -0,0 +1,18 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
import { importUser } from './testUtils'; |
|||
|
|||
test('Check usage limits', async ({ page }) => { |
|||
// Check without ID first
|
|||
await page.goto('./account'); |
|||
await expect(page.locator('div.bg-slate-100.rounded-md').filter({ hasText: 'Usage Limits' })).toBeHidden(); |
|||
|
|||
// Import user 01
|
|||
await importUser(page, '01'); |
|||
|
|||
// Verify that "Usage Limits" section is visible
|
|||
await expect(page.locator('#sectionUsageLimits')).toBeVisible(); |
|||
await expect(page.getByText('Your claims counter resets')).toBeVisible(); |
|||
await expect(page.getByText('Your registration counter resets')).toBeVisible(); |
|||
await expect(page.getByText('Your image counter resets')).toBeVisible(); |
|||
await expect(page.getByRole('button', { name: 'Recheck Limits' })).toBeVisible(); |
|||
}); |
@ -1,19 +0,0 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Create new ID from seed', async ({ page }) => { |
|||
// Create new ID using seed phrase "rigid shrug mobile…"
|
|||
await page.goto('./start'); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
|
|||
// Set name
|
|||
await page.getByRole('link', { name: 'Set Your Name' }).click(); |
|||
await page.getByPlaceholder('Name').click(); |
|||
await page.getByPlaceholder('Name').fill('User Zero'); |
|||
await page.getByRole('button', { name: 'Save Changes' }).click(); |
|||
|
|||
// Check DID
|
|||
await expect(page.getByRole('code')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'); |
|||
}); |
@ -1,19 +0,0 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Validate copy contact info to clipboard', async ({ page }) => { |
|||
// Create new ID using seed phrase "island fever beef…"
|
|||
await page.goto('./start'); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill('island fever beef wine urban aim vacant quit afford total poem flame service calm better adult neither color gaze forum month sister imitate excite'); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
|
|||
// Set name
|
|||
await page.getByRole('link', { name: 'Set Your Name' }).click(); |
|||
await page.getByPlaceholder('Name').click(); |
|||
await page.getByPlaceholder('Name').fill('User One'); |
|||
await page.getByRole('button', { name: 'Save Changes' }).click(); |
|||
|
|||
// Check DID
|
|||
await expect(page.getByRole('code')).toContainText('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39'); |
|||
}); |
@ -1,63 +0,0 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Create new project, then search for it', async ({ page }) => { |
|||
// Generate a random string of 16 characters
|
|||
let randomString = Math.random().toString(36).substring(2, 18); |
|||
|
|||
// In case the string is shorter than 16 characters, generate more characters until it is 16 characters long
|
|||
while (randomString.length < 16) { |
|||
randomString += Math.random().toString(36).substring(2, 18); |
|||
} |
|||
const finalRandomString = randomString.substring(0, 16); |
|||
|
|||
// Standard texts
|
|||
const standardTitle = "Idea "; |
|||
const standardDescription = "Description of Idea "; |
|||
|
|||
// Combine texts with the random string
|
|||
const finalTitle = standardTitle + finalRandomString; |
|||
const finalDescription = standardDescription + finalRandomString; |
|||
|
|||
// Create new ID using seed phrase "rigid shrug mobile…"
|
|||
await page.goto('./start'); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
|
|||
// Set name
|
|||
await page.getByRole('link', { name: 'Set Your Name' }).click(); |
|||
await page.getByPlaceholder('Name').click(); |
|||
await page.getByPlaceholder('Name').fill('User Zero'); |
|||
await page.getByRole('button', { name: 'Save Changes' }).click(); |
|||
|
|||
// Pause for 5 seconds
|
|||
await page.waitForTimeout(5000); // I have to wait, otherwise the (+) button to add a new project doesn't appear
|
|||
|
|||
// Create new project
|
|||
await page.goto('./projects'); |
|||
await page.getByRole('link', { name: 'Projects', exact: true }).click(); |
|||
await page.getByRole('button').click(); |
|||
await page.getByPlaceholder('Idea Name').click(); |
|||
await page.getByPlaceholder('Idea Name').fill(finalTitle); // Add random suffix
|
|||
await page.getByPlaceholder('Description').click(); |
|||
await page.getByPlaceholder('Description').fill(finalDescription); |
|||
await page.getByPlaceholder('Website').click(); |
|||
await page.getByPlaceholder('Website').fill('https://example.com'); |
|||
await page.getByPlaceholder('Start Date').click(); |
|||
await page.getByPlaceholder('Start Date').fill('2025-12-01'); |
|||
await page.getByPlaceholder('Start Time').click(); |
|||
await page.getByPlaceholder('Start Time').fill('12:00'); |
|||
await page.getByRole('button', { name: 'Save Project' }).click(); |
|||
|
|||
// Check texts
|
|||
await expect(page.locator('h2')).toContainText(finalTitle); |
|||
await expect(page.locator('#Content')).toContainText(finalDescription); |
|||
|
|||
// Search for project that was just created
|
|||
await page.goto('./discover'); |
|||
await page.waitForTimeout(5000); // Wait for a bit
|
|||
await page.getByPlaceholder('Search…').fill(finalRandomString); |
|||
await page.locator('#QuickSearch button').click(); |
|||
await expect(page.locator('section#Content li.border-b:nth-child(1)')).toContainText(finalRandomString); |
|||
}); |
@ -0,0 +1,40 @@ |
|||
import path from 'path'; |
|||
import { test, expect } from '@playwright/test'; |
|||
|
|||
test('Record item given from image-share', async ({ page }) => { |
|||
|
|||
let randomString = Math.random().toString(36).substring(2, 8); |
|||
|
|||
// Combine title prefix with the random string
|
|||
const finalTitle = `Gift ${randomString} from image-share`; |
|||
|
|||
// Create new ID using seed phrase "rigid shrug mobile…"
|
|||
await page.goto('./start'); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
|
|||
// Record something given
|
|||
await page.goto('./test'); |
|||
|
|||
const fileChooserPromise = page.waitForEvent('filechooser'); |
|||
await page.getByTestId('fileInput').click(); |
|||
const fileChooser = await fileChooserPromise; |
|||
await fileChooser.setFiles(path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png')); |
|||
await page.getByTestId('fileUploadButton').click(); |
|||
|
|||
// on shared photo page, choose the gift option
|
|||
await page.getByRole('button').filter({ hasText: /gift/i }).click(); |
|||
|
|||
await page.getByTestId('imagery').getByRole('img').isVisible(); |
|||
await page.getByPlaceholder('What was received').fill(finalTitle); |
|||
await page.getByRole('spinbutton').fill('2'); |
|||
await page.getByRole('button', { name: 'Sign & Send' }).click(); |
|||
await expect(page.getByText('That gift was recorded.')).toBeVisible(); |
|||
|
|||
|
|||
// Refresh home view and check gift
|
|||
await page.goto('./'); |
|||
const item1 = page.locator('li').filter({ hasText: finalTitle }); |
|||
await expect(item1.getByRole('img')).toBeVisible(); |
|||
}); |
@ -1,27 +1,86 @@ |
|||
import { test, expect } from '@playwright/test'; |
|||
import { importUser } from './testUtils'; |
|||
|
|||
test('Create new ID from seed', async ({ page }) => { |
|||
// Create new ID using seed phrase "rigid shrug mobile…"
|
|||
await page.goto('./start'); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
test('Add contact, record gift, confirm gift', async ({ page }) => { |
|||
// Generate a random string of 16 characters
|
|||
let randomString = Math.random().toString(36).substring(2, 18); |
|||
|
|||
// In case the string is shorter than 16 characters, generate more characters until it is 16 characters long
|
|||
while (randomString.length < 16) { |
|||
randomString += Math.random().toString(36).substring(2, 18); |
|||
} |
|||
const finalRandomString = randomString.substring(0, 16); |
|||
|
|||
// Generate a random non-zero single-digit number
|
|||
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1; |
|||
|
|||
// Set name
|
|||
await page.getByRole('link', { name: 'Set Your Name' }).click(); |
|||
await page.getByPlaceholder('Name').click(); |
|||
await page.getByPlaceholder('Name').fill('User Zero'); |
|||
await page.getByRole('button', { name: 'Save Changes' }).click(); |
|||
// Standard title prefix
|
|||
const standardTitle = "Gift "; |
|||
|
|||
// Add new contact 01
|
|||
// Combine title prefix with the random string
|
|||
const finalTitle = standardTitle + finalRandomString; |
|||
|
|||
// Contact name
|
|||
const contactName = 'Contact 00'; |
|||
|
|||
// Import user 01
|
|||
await importUser(page, '01'); |
|||
|
|||
// Add new contact 00
|
|||
await page.goto('./contacts'); |
|||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39'); |
|||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'); |
|||
await page.locator('button > svg.fa-plus').click(); |
|||
await expect(page.locator('div[role="alert"]')).toBeVisible(); |
|||
|
|||
// Why doesn't the alert box come up every time?
|
|||
// await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
|||
|
|||
await expect(page.locator('li.border-b')).toContainText('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39'); |
|||
// Verify added contact
|
|||
await expect(page.locator('li.border-b')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'); |
|||
|
|||
// Rename contact
|
|||
await page.locator('li.border-b h2 > button[title="Edit"]').click(); |
|||
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible(); |
|||
await page.getByPlaceholder('Name', { exact: true }).fill(contactName); |
|||
await page.locator('.dialog > .flex > button').first().click(); |
|||
|
|||
// Confirm that home shows contact in "Record Something…"
|
|||
await page.goto('./'); |
|||
await expect(page.locator('#sectionRecordSomethingGiven ul li').filter({ hasText: contactName }).nth(0)).toBeVisible(); |
|||
|
|||
// Record something given by new contact
|
|||
await page.getByRole('heading', { name: contactName }).click(); |
|||
await page.getByPlaceholder('What was given').fill(finalTitle); |
|||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString()); |
|||
await page.getByRole('button', { name: 'Sign & Send' }).click(); |
|||
await expect(page.getByText('That gift was recorded.')).toBeVisible(); |
|||
|
|||
// Refresh home view and check gift
|
|||
await page.goto('./'); |
|||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click(); |
|||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible(); |
|||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible(); |
|||
|
|||
// Switch to user 00
|
|||
await page.goto('./account'); |
|||
await page.getByRole('heading', { name: 'Advanced' }).click(); |
|||
await page.getByRole('link', { name: 'Switch Identifier' }).click(); |
|||
await page.getByRole('link', { name: 'Add Another Identity…' }).click(); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
|
|||
// Go to home view and look for gift
|
|||
await page.goto('./'); |
|||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click(); |
|||
|
|||
// Confirm gift as user 00
|
|||
await page.getByRole('button', { name: 'Confirm' }).click(); |
|||
await page.getByRole('button', { name: 'Yes' }).click(); |
|||
await expect(page.getByText('Confirmation submitted.')).toBeVisible(); |
|||
|
|||
// Refresh claim page, Confirm button should be hidden
|
|||
await page.reload(); |
|||
await expect(page.getByRole('button', { name: 'Confirm' })).toBeVisible(); |
|||
await expect(page.getByRole('button', { name: 'Confirm' })).toBeHidden(); |
|||
}); |
@ -0,0 +1,32 @@ |
|||
import { expect } from '@playwright/test'; |
|||
|
|||
export async function importUser(page, id) { |
|||
let seedPhrase, userName, did; |
|||
|
|||
// Set seed phrase and DID based on user ID
|
|||
switch(id) { |
|||
case '01': |
|||
seedPhrase = 'island fever beef wine urban aim vacant quit afford total poem flame service calm better adult neither color gaze forum month sister imitate excite'; |
|||
userName = 'User One'; |
|||
did = 'did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39'; |
|||
break; |
|||
default: // to user 00
|
|||
seedPhrase = 'rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'; |
|||
userName = 'User Zero'; |
|||
did = 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'; |
|||
} |
|||
|
|||
// Import ID
|
|||
await page.goto('./start'); |
|||
await page.getByText('You have a seed').click(); |
|||
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase); |
|||
await page.getByRole('button', { name: 'Import' }).click(); |
|||
|
|||
// Set name
|
|||
await page.getByRole('link', { name: 'Set Your Name' }).click(); |
|||
await page.getByPlaceholder('Name').fill(userName); |
|||
await page.getByRole('button', { name: 'Save Changes' }).click(); |
|||
|
|||
// Check DID
|
|||
await expect(page.getByRole('code')).toContainText(did); |
|||
} |
@ -1,437 +0,0 @@ |
|||
import { test, expect, type Page } from '@playwright/test'; |
|||
|
|||
test.beforeEach(async ({ page }) => { |
|||
await page.goto('https://demo.playwright.dev/todomvc'); |
|||
}); |
|||
|
|||
const TODO_ITEMS = [ |
|||
'buy some cheese', |
|||
'feed the cat', |
|||
'book a doctors appointment' |
|||
] as const; |
|||
|
|||
test.describe('New Todo', () => { |
|||
test('should allow me to add todo items', async ({ page }) => { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
// Create 1st todo.
|
|||
await newTodo.fill(TODO_ITEMS[0]); |
|||
await newTodo.press('Enter'); |
|||
|
|||
// Make sure the list only has one todo item.
|
|||
await expect(page.getByTestId('todo-title')).toHaveText([ |
|||
TODO_ITEMS[0] |
|||
]); |
|||
|
|||
// Create 2nd todo.
|
|||
await newTodo.fill(TODO_ITEMS[1]); |
|||
await newTodo.press('Enter'); |
|||
|
|||
// Make sure the list now has two todo items.
|
|||
await expect(page.getByTestId('todo-title')).toHaveText([ |
|||
TODO_ITEMS[0], |
|||
TODO_ITEMS[1] |
|||
]); |
|||
|
|||
await checkNumberOfTodosInLocalStorage(page, 2); |
|||
}); |
|||
|
|||
test('should clear text input field when an item is added', async ({ page }) => { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
// Create one todo item.
|
|||
await newTodo.fill(TODO_ITEMS[0]); |
|||
await newTodo.press('Enter'); |
|||
|
|||
// Check that input is empty.
|
|||
await expect(newTodo).toBeEmpty(); |
|||
await checkNumberOfTodosInLocalStorage(page, 1); |
|||
}); |
|||
|
|||
test('should append new items to the bottom of the list', async ({ page }) => { |
|||
// Create 3 items.
|
|||
await createDefaultTodos(page); |
|||
|
|||
// create a todo count locator
|
|||
const todoCount = page.getByTestId('todo-count') |
|||
|
|||
// Check test using different methods.
|
|||
await expect(page.getByText('3 items left')).toBeVisible(); |
|||
await expect(todoCount).toHaveText('3 items left'); |
|||
await expect(todoCount).toContainText('3'); |
|||
await expect(todoCount).toHaveText(/3/); |
|||
|
|||
// Check all items in one call.
|
|||
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); |
|||
await checkNumberOfTodosInLocalStorage(page, 3); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Mark all as completed', () => { |
|||
test.beforeEach(async ({ page }) => { |
|||
await createDefaultTodos(page); |
|||
await checkNumberOfTodosInLocalStorage(page, 3); |
|||
}); |
|||
|
|||
test.afterEach(async ({ page }) => { |
|||
await checkNumberOfTodosInLocalStorage(page, 3); |
|||
}); |
|||
|
|||
test('should allow me to mark all items as completed', async ({ page }) => { |
|||
// Complete all todos.
|
|||
await page.getByLabel('Mark all as complete').check(); |
|||
|
|||
// Ensure all todos have 'completed' class.
|
|||
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 3); |
|||
}); |
|||
|
|||
test('should allow me to clear the complete state of all items', async ({ page }) => { |
|||
const toggleAll = page.getByLabel('Mark all as complete'); |
|||
// Check and then immediately uncheck.
|
|||
await toggleAll.check(); |
|||
await toggleAll.uncheck(); |
|||
|
|||
// Should be no completed classes.
|
|||
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); |
|||
}); |
|||
|
|||
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { |
|||
const toggleAll = page.getByLabel('Mark all as complete'); |
|||
await toggleAll.check(); |
|||
await expect(toggleAll).toBeChecked(); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 3); |
|||
|
|||
// Uncheck first todo.
|
|||
const firstTodo = page.getByTestId('todo-item').nth(0); |
|||
await firstTodo.getByRole('checkbox').uncheck(); |
|||
|
|||
// Reuse toggleAll locator and make sure its not checked.
|
|||
await expect(toggleAll).not.toBeChecked(); |
|||
|
|||
await firstTodo.getByRole('checkbox').check(); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 3); |
|||
|
|||
// Assert the toggle all is checked again.
|
|||
await expect(toggleAll).toBeChecked(); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Item', () => { |
|||
|
|||
test('should allow me to mark items as complete', async ({ page }) => { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
// Create two items.
|
|||
for (const item of TODO_ITEMS.slice(0, 2)) { |
|||
await newTodo.fill(item); |
|||
await newTodo.press('Enter'); |
|||
} |
|||
|
|||
// Check first item.
|
|||
const firstTodo = page.getByTestId('todo-item').nth(0); |
|||
await firstTodo.getByRole('checkbox').check(); |
|||
await expect(firstTodo).toHaveClass('completed'); |
|||
|
|||
// Check second item.
|
|||
const secondTodo = page.getByTestId('todo-item').nth(1); |
|||
await expect(secondTodo).not.toHaveClass('completed'); |
|||
await secondTodo.getByRole('checkbox').check(); |
|||
|
|||
// Assert completed class.
|
|||
await expect(firstTodo).toHaveClass('completed'); |
|||
await expect(secondTodo).toHaveClass('completed'); |
|||
}); |
|||
|
|||
test('should allow me to un-mark items as complete', async ({ page }) => { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
// Create two items.
|
|||
for (const item of TODO_ITEMS.slice(0, 2)) { |
|||
await newTodo.fill(item); |
|||
await newTodo.press('Enter'); |
|||
} |
|||
|
|||
const firstTodo = page.getByTestId('todo-item').nth(0); |
|||
const secondTodo = page.getByTestId('todo-item').nth(1); |
|||
const firstTodoCheckbox = firstTodo.getByRole('checkbox'); |
|||
|
|||
await firstTodoCheckbox.check(); |
|||
await expect(firstTodo).toHaveClass('completed'); |
|||
await expect(secondTodo).not.toHaveClass('completed'); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 1); |
|||
|
|||
await firstTodoCheckbox.uncheck(); |
|||
await expect(firstTodo).not.toHaveClass('completed'); |
|||
await expect(secondTodo).not.toHaveClass('completed'); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 0); |
|||
}); |
|||
|
|||
test('should allow me to edit an item', async ({ page }) => { |
|||
await createDefaultTodos(page); |
|||
|
|||
const todoItems = page.getByTestId('todo-item'); |
|||
const secondTodo = todoItems.nth(1); |
|||
await secondTodo.dblclick(); |
|||
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); |
|||
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); |
|||
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); |
|||
|
|||
// Explicitly assert the new text value.
|
|||
await expect(todoItems).toHaveText([ |
|||
TODO_ITEMS[0], |
|||
'buy some sausages', |
|||
TODO_ITEMS[2] |
|||
]); |
|||
await checkTodosInLocalStorage(page, 'buy some sausages'); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Editing', () => { |
|||
test.beforeEach(async ({ page }) => { |
|||
await createDefaultTodos(page); |
|||
await checkNumberOfTodosInLocalStorage(page, 3); |
|||
}); |
|||
|
|||
test('should hide other controls when editing', async ({ page }) => { |
|||
const todoItem = page.getByTestId('todo-item').nth(1); |
|||
await todoItem.dblclick(); |
|||
await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); |
|||
await expect(todoItem.locator('label', { |
|||
hasText: TODO_ITEMS[1], |
|||
})).not.toBeVisible(); |
|||
await checkNumberOfTodosInLocalStorage(page, 3); |
|||
}); |
|||
|
|||
test('should save edits on blur', async ({ page }) => { |
|||
const todoItems = page.getByTestId('todo-item'); |
|||
await todoItems.nth(1).dblclick(); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); |
|||
|
|||
await expect(todoItems).toHaveText([ |
|||
TODO_ITEMS[0], |
|||
'buy some sausages', |
|||
TODO_ITEMS[2], |
|||
]); |
|||
await checkTodosInLocalStorage(page, 'buy some sausages'); |
|||
}); |
|||
|
|||
test('should trim entered text', async ({ page }) => { |
|||
const todoItems = page.getByTestId('todo-item'); |
|||
await todoItems.nth(1).dblclick(); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); |
|||
|
|||
await expect(todoItems).toHaveText([ |
|||
TODO_ITEMS[0], |
|||
'buy some sausages', |
|||
TODO_ITEMS[2], |
|||
]); |
|||
await checkTodosInLocalStorage(page, 'buy some sausages'); |
|||
}); |
|||
|
|||
test('should remove the item if an empty text string was entered', async ({ page }) => { |
|||
const todoItems = page.getByTestId('todo-item'); |
|||
await todoItems.nth(1).dblclick(); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); |
|||
|
|||
await expect(todoItems).toHaveText([ |
|||
TODO_ITEMS[0], |
|||
TODO_ITEMS[2], |
|||
]); |
|||
}); |
|||
|
|||
test('should cancel edits on escape', async ({ page }) => { |
|||
const todoItems = page.getByTestId('todo-item'); |
|||
await todoItems.nth(1).dblclick(); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); |
|||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); |
|||
await expect(todoItems).toHaveText(TODO_ITEMS); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Counter', () => { |
|||
test('should display the current number of todo items', async ({ page }) => { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
// create a todo count locator
|
|||
const todoCount = page.getByTestId('todo-count') |
|||
|
|||
await newTodo.fill(TODO_ITEMS[0]); |
|||
await newTodo.press('Enter'); |
|||
|
|||
await expect(todoCount).toContainText('1'); |
|||
|
|||
await newTodo.fill(TODO_ITEMS[1]); |
|||
await newTodo.press('Enter'); |
|||
await expect(todoCount).toContainText('2'); |
|||
|
|||
await checkNumberOfTodosInLocalStorage(page, 2); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Clear completed button', () => { |
|||
test.beforeEach(async ({ page }) => { |
|||
await createDefaultTodos(page); |
|||
}); |
|||
|
|||
test('should display the correct text', async ({ page }) => { |
|||
await page.locator('.todo-list li .toggle').first().check(); |
|||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); |
|||
}); |
|||
|
|||
test('should remove completed items when clicked', async ({ page }) => { |
|||
const todoItems = page.getByTestId('todo-item'); |
|||
await todoItems.nth(1).getByRole('checkbox').check(); |
|||
await page.getByRole('button', { name: 'Clear completed' }).click(); |
|||
await expect(todoItems).toHaveCount(2); |
|||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); |
|||
}); |
|||
|
|||
test('should be hidden when there are no items that are completed', async ({ page }) => { |
|||
await page.locator('.todo-list li .toggle').first().check(); |
|||
await page.getByRole('button', { name: 'Clear completed' }).click(); |
|||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Persistence', () => { |
|||
test('should persist its data', async ({ page }) => { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
for (const item of TODO_ITEMS.slice(0, 2)) { |
|||
await newTodo.fill(item); |
|||
await newTodo.press('Enter'); |
|||
} |
|||
|
|||
const todoItems = page.getByTestId('todo-item'); |
|||
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); |
|||
await firstTodoCheck.check(); |
|||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); |
|||
await expect(firstTodoCheck).toBeChecked(); |
|||
await expect(todoItems).toHaveClass(['completed', '']); |
|||
|
|||
// Ensure there is 1 completed item.
|
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 1); |
|||
|
|||
// Now reload.
|
|||
await page.reload(); |
|||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); |
|||
await expect(firstTodoCheck).toBeChecked(); |
|||
await expect(todoItems).toHaveClass(['completed', '']); |
|||
}); |
|||
}); |
|||
|
|||
test.describe('Routing', () => { |
|||
test.beforeEach(async ({ page }) => { |
|||
await createDefaultTodos(page); |
|||
// make sure the app had a chance to save updated todos in storage
|
|||
// before navigating to a new view, otherwise the items can get lost :(
|
|||
// in some frameworks like Durandal
|
|||
await checkTodosInLocalStorage(page, TODO_ITEMS[0]); |
|||
}); |
|||
|
|||
test('should allow me to display active items', async ({ page }) => { |
|||
const todoItem = page.getByTestId('todo-item'); |
|||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); |
|||
|
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 1); |
|||
await page.getByRole('link', { name: 'Active' }).click(); |
|||
await expect(todoItem).toHaveCount(2); |
|||
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); |
|||
}); |
|||
|
|||
test('should respect the back button', async ({ page }) => { |
|||
const todoItem = page.getByTestId('todo-item'); |
|||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); |
|||
|
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 1); |
|||
|
|||
await test.step('Showing all items', async () => { |
|||
await page.getByRole('link', { name: 'All' }).click(); |
|||
await expect(todoItem).toHaveCount(3); |
|||
}); |
|||
|
|||
await test.step('Showing active items', async () => { |
|||
await page.getByRole('link', { name: 'Active' }).click(); |
|||
}); |
|||
|
|||
await test.step('Showing completed items', async () => { |
|||
await page.getByRole('link', { name: 'Completed' }).click(); |
|||
}); |
|||
|
|||
await expect(todoItem).toHaveCount(1); |
|||
await page.goBack(); |
|||
await expect(todoItem).toHaveCount(2); |
|||
await page.goBack(); |
|||
await expect(todoItem).toHaveCount(3); |
|||
}); |
|||
|
|||
test('should allow me to display completed items', async ({ page }) => { |
|||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 1); |
|||
await page.getByRole('link', { name: 'Completed' }).click(); |
|||
await expect(page.getByTestId('todo-item')).toHaveCount(1); |
|||
}); |
|||
|
|||
test('should allow me to display all items', async ({ page }) => { |
|||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); |
|||
await checkNumberOfCompletedTodosInLocalStorage(page, 1); |
|||
await page.getByRole('link', { name: 'Active' }).click(); |
|||
await page.getByRole('link', { name: 'Completed' }).click(); |
|||
await page.getByRole('link', { name: 'All' }).click(); |
|||
await expect(page.getByTestId('todo-item')).toHaveCount(3); |
|||
}); |
|||
|
|||
test('should highlight the currently applied filter', async ({ page }) => { |
|||
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); |
|||
|
|||
//create locators for active and completed links
|
|||
const activeLink = page.getByRole('link', { name: 'Active' }); |
|||
const completedLink = page.getByRole('link', { name: 'Completed' }); |
|||
await activeLink.click(); |
|||
|
|||
// Page change - active items.
|
|||
await expect(activeLink).toHaveClass('selected'); |
|||
await completedLink.click(); |
|||
|
|||
// Page change - completed items.
|
|||
await expect(completedLink).toHaveClass('selected'); |
|||
}); |
|||
}); |
|||
|
|||
async function createDefaultTodos(page: Page) { |
|||
// create a new todo locator
|
|||
const newTodo = page.getByPlaceholder('What needs to be done?'); |
|||
|
|||
for (const item of TODO_ITEMS) { |
|||
await newTodo.fill(item); |
|||
await newTodo.press('Enter'); |
|||
} |
|||
} |
|||
|
|||
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { |
|||
return await page.waitForFunction(e => { |
|||
return JSON.parse(localStorage['react-todos']).length === e; |
|||
}, expected); |
|||
} |
|||
|
|||
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { |
|||
return await page.waitForFunction(e => { |
|||
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; |
|||
}, expected); |
|||
} |
|||
|
|||
async function checkTodosInLocalStorage(page: Page, title: string) { |
|||
return await page.waitForFunction(t => { |
|||
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); |
|||
}, title); |
|||
} |
Loading…
Reference in new issue