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_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H |
||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch |
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch |
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app |
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 { test, expect } from '@playwright/test'; |
||||
|
import { importUser } from './testUtils'; |
||||
|
|
||||
test('Create new ID from seed', async ({ page }) => { |
test('Add contact, record gift, confirm gift', async ({ page }) => { |
||||
// Create new ID using seed phrase "rigid shrug mobile…"
|
// Generate a random string of 16 characters
|
||||
await page.goto('./start'); |
let randomString = Math.random().toString(36).substring(2, 18); |
||||
await page.getByText('You have a seed').click(); |
|
||||
await page.getByPlaceholder('Seed Phrase').click(); |
// In case the string is shorter than 16 characters, generate more characters until it is 16 characters long
|
||||
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'); |
while (randomString.length < 16) { |
||||
await page.getByRole('button', { name: 'Import' }).click(); |
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
|
// Standard title prefix
|
||||
await page.getByRole('link', { name: 'Set Your Name' }).click(); |
const standardTitle = "Gift "; |
||||
await page.getByPlaceholder('Name').click(); |
|
||||
await page.getByPlaceholder('Name').fill('User Zero'); |
|
||||
await page.getByRole('button', { name: 'Save Changes' }).click(); |
|
||||
|
|
||||
// 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.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 page.locator('button > svg.fa-plus').click(); |
||||
await expect(page.locator('div[role="alert"]')).toBeVisible(); |
await expect(page.locator('div[role="alert"]')).toBeVisible(); |
||||
|
|
||||
// Why doesn't the alert box come up every time?
|
// Why doesn't the alert box come up every time?
|
||||
// await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
// 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