Browse Source

refactor invite link & add test

Trent Larson 2 months ago
parent
commit
d82e0db951
  1. 4
      playwright.config-local.ts
  2. 17
      src/components/InviteDialog.vue
  3. 3
      src/views/ContactsView.vue
  4. 27
      src/views/InviteOneView.vue
  5. 6
      test-playwright/00-noid-tests.spec.ts
  6. 32
      test-playwright/05-invite.spec.ts
  7. 2
      test-playwright/20-create-project.spec.ts
  8. 25
      test-playwright/testUtils.ts

4
playwright.config-local.ts

@ -74,7 +74,7 @@ export default defineConfig({
/* Configure global timeout; default is 30000 milliseconds */ /* Configure global timeout; default is 30000 milliseconds */
// the image upload will often not succeed at 5 seconds // the image upload will often not succeed at 5 seconds
// timeout: 5000, // timeout: 10000,
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
/** /**
@ -91,7 +91,7 @@ export default defineConfig({
*/ */
webServer: { webServer: {
command: command:
"VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run dev", "VITE_APP_SERVER=http://localhost:8080 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_PASSKEYS_ENABLED=true npm run dev",
url: "http://localhost:8080", url: "http://localhost:8080",
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },

17
src/components/InviteDialog.vue

@ -1,12 +1,14 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">{{ title }}</h1> <h1 class="text-xl font-bold text-center mb-4">Invitation & Notes</h1>
{{ message }} These are optional notes for your use, to make comments for to recall
later when redeemed by someone. These notes are sent to the server. If you
want to store your own way, the invitation ID is: {{ inviteIdentifier }}
<input <input
type="text" type="text"
placeholder="Name" placeholder="Notes"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="text" v-model="text"
/> />
@ -52,22 +54,19 @@ export default class InviteDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (text: string, expiresAt: string) => void = () => {}; callback: (text: string, expiresAt: string) => void = () => {};
message = ""; inviteIdentifier = "";
text = ""; text = "";
title = "";
visible = false; visible = false;
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 3) expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 3)
.toISOString() .toISOString()
.substring(0, 10); .substring(0, 10);
async open( async open(
title: string, inviteIdentifier: string,
message: string,
aCallback: (text: string, expiresAt: string) => void, aCallback: (text: string, expiresAt: string) => void,
) { ) {
this.callback = aCallback; this.callback = aCallback;
this.title = title; this.inviteIdentifier = inviteIdentifier;
this.message = message;
this.visible = true; this.visible = true;
} }

3
src/views/ContactsView.vue

@ -460,7 +460,8 @@ export default class ContactsView extends Vue {
registered: true, registered: true,
}), }),
); );
} catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error redeeming invite:", error); console.error("Error redeeming invite:", error);
let message = "Got an error sending the invite."; let message = "Got an error sending the invite.";
if ( if (

27
src/views/InviteOneView.vue

@ -46,11 +46,14 @@
> >
<td <td
class="py-2 text-center text-blue-500" class="py-2 text-center text-blue-500"
@click="copyInviteAndNotify(invite.jwt)" @click="copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)"
title="{{ inviteLink(invite.jwt) }}"
> >
{{ getTruncatedInviteId(invite.inviteIdentifier) }} {{ getTruncatedInviteId(invite.inviteIdentifier) }}
</td> </td>
<td class="py-2 text-left">{{ invite.notes }}</td> <td class="py-2 text-left" :data-testId="inviteLink(invite.jwt)">
{{ invite.notes }}
</td>
<td class="py-2 text-center"> <td class="py-2 text-center">
{{ invite.expiresAt.substring(0, 10) }} {{ invite.expiresAt.substring(0, 10) }}
</td> </td>
@ -134,31 +137,31 @@ export default class InviteOneView extends Vue {
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`; return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
} }
copyInviteAndNotify(jwt: string) { inviteLink(jwt: string): string {
const link = APP_SERVER + "/contacts?inviteJwt=" + jwt; return APP_SERVER + "/contacts?inviteJwt=" + jwt;
useClipboard().copy(link); }
copyInviteAndNotify(inviteId: string, jwt: string) {
useClipboard().copy(this.inviteLink(jwt));
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Copied", title: "Copied",
text: "Invitation link is copied to clipboard.", text: "Link for invite " + inviteId + " is copied to clipboard.",
}, },
3000, 3000,
); );
} }
async createInvite() { async createInvite() {
(this.$refs.inviteDialog as InviteDialog).open(
"Invitation Note",
`These notes are only for your use, to make comments for a link to recall later if redeemed by someone.
Note that this is sent to the server.`,
async (notes, expiresAt) => {
try {
const inviteIdentifier = const inviteIdentifier =
Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2); Math.random().toString(36).substring(2);
(this.$refs.inviteDialog as InviteDialog).open(
inviteIdentifier,
async (notes, expiresAt) => {
try {
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
if (!expiresAt) { if (!expiresAt) {
throw { throw {

6
test-playwright/00-noid-tests.spec.ts

@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { deleteContact, generateEthrUser, importUser } from './testUtils'; import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
test('Check activity feed', async ({ page }) => { test('Check activity feed', async ({ page }) => {
// Load app homepage // Load app homepage
@ -112,12 +112,12 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
test('Check User 0 can register a random person', async ({ page }) => { test('Check User 0 can register a random person', async ({ page }) => {
await importUser(page, '00'); await importUser(page, '00');
const newDid = await generateEthrUser(page); const newDid = await generateAndRegisterEthrUser(page);
expect(newDid).toContain('did:ethr:'); expect(newDid).toContain('did:ethr:');
await page.goto('./'); await page.goto('./');
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click(); await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
await page.getByPlaceholder('What was given').fill('Access!'); await page.getByPlaceholder('What was given').fill('Gave me access!');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible(); await expect(page.getByText('That gift was recorded.')).toBeVisible();
// now ensure that alert goes away // now ensure that alert goes away

32
test-playwright/05-invite.spec.ts

@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
test('Check User 0 can invite someone', async ({ page }) => {
const newDid = await generateNewEthrUser(page);
await importUser(page, '00');
await page.goto('./invite-one');
await page.locator('button > svg.fa-plus').click();
const neighborNum = await generateRandomString(5);
await page.getByPlaceholder('Notes', { exact: true }).fill(`Neighbor ${neighborNum}`);
// get the expiration date input and set to 14 days from now
const expirationDate = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
await page.locator('input[type="date"]').fill(expirationDate.toISOString().split('T')[0]);
await page.locator('button:has-text("Save")').click();
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
// check that the invite is in the list
const newInviteLine = await page.locator(`td:has-text("Neighbor ${neighborNum}")`);
await expect(newInviteLine).toBeVisible();
// retrieve the link from the title
const inviteLink = await newInviteLine.getAttribute('data-testId');
await expect(inviteLink).not.toBeNull();
// become the new user and accept the invite
await switchToUser(page, newDid);
await page.goto(inviteLink as string);
await page.getByPlaceholder('Name', { exact: true }).fill(`My pal User #0`);
await page.locator('button:has-text("Save")').click();
await expect(page.locator('button:has-text("Save")')).toBeHidden();
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
});

2
test-playwright/20-create-project.spec.ts

@ -49,7 +49,7 @@ test('Create new project, then search for it', async ({ page }) => {
// Create new project // Create new project
await page.goto('./projects'); await page.goto('./projects');
await page.getByRole('link', { name: 'Projects', exact: true }).click(); await page.getByRole('link', { name: 'Projects', exact: true }).click();
await page.getByRole('button').click(); await page.locator('button > svg.fa-plus').click();
await page.getByPlaceholder('Idea Name').fill(finalTitle); await page.getByPlaceholder('Idea Name').fill(finalTitle);
await page.getByPlaceholder('Description').fill(finalDescription); await page.getByPlaceholder('Description').fill(finalDescription);
await page.getByPlaceholder('Website').fill(standardWebsite); await page.getByPlaceholder('Website').fill(standardWebsite);

25
test-playwright/testUtils.ts

@ -34,10 +34,15 @@ export async function importUser(page: Page, id?: string): Promise<string> {
// This is to switch to someone already in the identity table. It doesn't include registration. // This is to switch to someone already in the identity table. It doesn't include registration.
export async function switchToUser(page: Page, did: string): Promise<void> { export async function switchToUser(page: Page, did: string): Promise<void> {
await page.goto('./account'); // await page.goto('./account');
await page.getByRole('heading', { name: 'Advanced' }).click(); // await page.getByRole('heading', { name: 'Advanced' }).click();
await page.getByRole('link', { name: 'Switch Identifier' }).click(); // await page.getByRole('link', { name: 'Switch Identifier' }).click();
await page.getByRole('code', { name: did }).click(); await page.goto('./identity-switcher');
const didElem = await page.locator(`code:has-text("${did}")`);
await didElem.isVisible();
await didElem.click();
// wait for the switch to happen and the account page to fully load
await page.getByTestId('didWrapper').locator('code:has-text("did:")');
} }
function createContactName(did: string): string { function createContactName(did: string): string {
@ -56,17 +61,21 @@ export async function deleteContact(page: Page, did: string): Promise<void> {
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden(); await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
} }
// Generate a new random user and register them. export async function generateNewEthrUser(page: Page): Promise<string> {
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
export async function generateEthrUser(page: Page): Promise<string> {
await page.goto('./start'); await page.goto('./start');
await page.getByTestId('newSeed').click(); await page.getByTestId('newSeed').click();
await expect(page.locator('span:has-text("Created")')).toBeVisible(); await expect(page.locator('span:has-text("Created")')).toBeVisible();
await page.goto('./account'); await page.goto('./account');
// wait until the DID shows on the page in the 'did' element
const didElem = await page.getByTestId('didWrapper').locator('code:has-text("did:")'); const didElem = await page.getByTestId('didWrapper').locator('code:has-text("did:")');
const newDid = await didElem.innerText(); const newDid = await didElem.innerText();
return newDid;
}
// Generate a new random user and register them.
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
const newDid = await generateNewEthrUser(page);
await importUser(page, '000'); // switch to user 000 await importUser(page, '000'); // switch to user 000

Loading…
Cancel
Save