forked from trent_larson/crowd-funder-for-time-pwa
refactor invite link & add test
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
const inviteIdentifier =
|
||||||
|
Math.random().toString(36).substring(2) +
|
||||||
|
Math.random().toString(36).substring(2) +
|
||||||
|
Math.random().toString(36).substring(2);
|
||||||
(this.$refs.inviteDialog as InviteDialog).open(
|
(this.$refs.inviteDialog as InviteDialog).open(
|
||||||
"Invitation Note",
|
inviteIdentifier,
|
||||||
`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) => {
|
async (notes, expiresAt) => {
|
||||||
try {
|
try {
|
||||||
const inviteIdentifier =
|
|
||||||
Math.random().toString(36).substring(2) +
|
|
||||||
Math.random().toString(36).substring(2) +
|
|
||||||
Math.random().toString(36).substring(2);
|
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
if (!expiresAt) {
|
if (!expiresAt) {
|
||||||
throw {
|
throw {
|
||||||
|
|||||||
@@ -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
Normal file
32
test-playwright/05-invite.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user