Skip to content

Commit 7cafa20

Browse files
authored
Restore unsaved feedback using localStorage (#740)
* client: restore unsaved feedback * client: add E2E tests * client: update packages
1 parent 53225f5 commit 7cafa20

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2352
-2318
lines changed

client/e2e/pages/feedback-history-details.page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Page, expect } from '@playwright/test';
22
import { Persona } from './sign-in.page';
33

44
type Details = {
5-
message?: string; // note: there's no message for spontaneous feedback
5+
message?: string; // note: optional because there's no "message" for spontaneous feedback
66
context: string;
77
positive: string;
88
negative: string;

client/e2e/pages/feedback-history.page.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ export class FeedbackHistoryPage {
1313
}
1414
}
1515

16-
async findDetailsLink(persona: Persona) {
16+
findDetailsLink(persona: Persona) {
17+
return this.findLink(persona, 'Consulter');
18+
}
19+
20+
findReplyLink(persona: Persona) {
21+
return this.findLink(persona, 'Répondre');
22+
}
23+
24+
private async findLink(persona: Persona, label: 'Consulter' | 'Répondre') {
1725
// Wait until the the `<table>` is rendered
1826
await this.page.locator('tbody').waitFor();
1927

20-
return this.page
21-
.locator('tbody > tr', { has: this.page.getByRole('cell', { name: persona }) })
22-
.getByLabel('Consulter');
28+
return this.page.locator('tbody > tr', { has: this.page.getByRole('cell', { name: persona }) }).getByLabel(label);
2329
}
2430
}

client/e2e/pages/give-requested-feedback.page.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Page, expect } from '@playwright/test';
22
import { Persona } from './sign-in.page';
33

44
type Details = {
5-
message: string;
65
context: string;
76
positive: string;
87
negative: string;
@@ -12,22 +11,76 @@ type Details = {
1211
export class GiveRequestedFeedbackPage {
1312
constructor(private page: Page) {}
1413

15-
async give(persona: Persona, details: Details) {
14+
async fill(details: Partial<Details>) {
15+
await this.page.waitForURL('/fr/give-requested/token/**');
16+
17+
if (details.context !== undefined) {
18+
await this.page.getByRole('textbox', { name: 'Contexte' }).fill(details.context);
19+
}
20+
21+
if (details.positive !== undefined) {
22+
await this.page.getByRole('textbox', { name: 'Points forts' }).fill(details.positive);
23+
}
24+
25+
if (details.negative !== undefined) {
26+
await this.page.getByRole('textbox', { name: "Axes d'améliorations" }).fill(details.negative);
27+
}
28+
29+
if (details.comment !== undefined) {
30+
await this.page.getByRole('textbox', { name: 'Commentaire' }).fill(details.comment);
31+
}
32+
}
33+
34+
async expect(persona: Persona, details: Partial<Details>) {
1635
await this.page.waitForURL('/fr/give-requested/token/**');
1736

1837
await expect(
19-
this.page.getByLabel('Email de votre collègue'),
20-
'Feedback receiver should be filled in correctly',
38+
this.page.getByRole('textbox', { name: 'Email de votre collègue' }),
39+
`Feedback receiver should be ${persona ? persona : 'empty'}`,
2140
).toHaveValue(persona);
2241

23-
await this.page.getByText('Contexte').fill(details.context);
24-
await this.page.getByText('Points forts').fill(details.positive);
25-
await this.page.getByText("Axes d'améliorations").fill(details.negative);
26-
await this.page.getByText('Commentaire').fill(details.comment);
42+
if (details.context !== undefined) {
43+
await expect(
44+
this.page.getByRole('textbox', { name: 'Contexte' }),
45+
'Feedback "context" should be visible',
46+
).toHaveValue(details.context);
47+
}
48+
49+
if (details.positive !== undefined) {
50+
await expect(
51+
this.page.getByRole('textbox', { name: 'Points forts' }),
52+
'Feedback "positive" should be visible',
53+
).toHaveValue(details.positive);
54+
}
2755

56+
if (details.negative !== undefined) {
57+
await expect(
58+
this.page.getByRole('textbox', { name: "Axes d'améliorations" }),
59+
'Feedback "negative" should be visible',
60+
).toHaveValue(details.negative);
61+
}
62+
63+
if (details.comment !== undefined) {
64+
await expect(
65+
this.page.getByRole('textbox', { name: 'Commentaire' }),
66+
'Feedback "comment" should be visible',
67+
).toHaveValue(details.comment);
68+
}
69+
}
70+
71+
async save() {
72+
await this.page.getByRole('button', { name: 'Sauvegarder' }).click();
73+
}
74+
75+
async submit() {
2876
await this.page.getByRole('button', { name: 'Envoyer' }).click();
2977
await this.page.getByRole('button', { name: 'Confirmer' }).click();
3078

3179
await this.page.waitForURL('/fr/give-requested/success');
3280
}
81+
82+
async fillAndSubmit(details: Details) {
83+
await this.fill(details);
84+
await this.submit();
85+
}
3386
}
Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Page } from '@playwright/test';
1+
import { Page, expect } from '@playwright/test';
22
import { Persona } from './sign-in.page';
33

44
type Details = {
@@ -12,25 +12,107 @@ export class GiveSpontaneousFeedbackPage {
1212
constructor(private page: Page) {}
1313

1414
async goto() {
15-
await this.page.goto(`/fr/give`);
15+
await this.page.goto('/fr/give');
16+
17+
await this.page.waitForURL('/fr/give');
1618
}
1719

18-
async give(persona: Persona, details: Details) {
19-
await this.page.getByLabel('Email de votre collègue').fill(persona);
20+
async fill(persona: Persona | '' | undefined, details: Partial<Details>) {
21+
if (persona !== undefined) {
22+
await this.page.getByRole('combobox', { name: 'Email de votre collègue' }).fill(persona);
23+
}
24+
25+
if (details.context !== undefined) {
26+
await this.page.getByRole('textbox', { name: 'Contexte' }).fill(details.context);
27+
}
28+
29+
if (details.positive !== undefined) {
30+
await this.page.getByRole('textbox', { name: 'Points forts' }).fill(details.positive);
31+
}
32+
33+
if (details.negative !== undefined) {
34+
await this.page.getByRole('textbox', { name: "Axes d'améliorations" }).fill(details.negative);
35+
}
36+
37+
if (details.comment !== undefined) {
38+
await this.page.getByRole('textbox', { name: 'Commentaire' }).fill(details.comment);
39+
}
40+
}
2041

21-
await this.page.getByText('Contexte').fill(details.context);
22-
await this.page.getByText('Points forts').fill(details.positive);
23-
await this.page.getByText("Axes d'améliorations").fill(details.negative);
24-
await this.page.getByText('Commentaire').fill(details.comment);
42+
async expect(persona: Persona | '' | undefined, details: Partial<Details>) {
43+
if (persona !== undefined) {
44+
await expect(
45+
this.page.getByRole('combobox', { name: 'Email de votre collègue' }),
46+
`Feedback receiver should be ${persona ? persona : 'empty'}`,
47+
).toHaveValue(persona);
48+
}
49+
50+
if (details.context !== undefined) {
51+
await expect(
52+
this.page.getByRole('textbox', { name: 'Contexte' }),
53+
'Feedback "context" should be visible',
54+
).toHaveValue(details.context);
55+
}
56+
57+
if (details.positive !== undefined) {
58+
await expect(
59+
this.page.getByRole('textbox', { name: 'Points forts' }),
60+
'Feedback "positive" should be visible',
61+
).toHaveValue(details.positive);
62+
}
63+
64+
if (details.negative !== undefined) {
65+
await expect(
66+
this.page.getByRole('textbox', { name: "Axes d'améliorations" }),
67+
'Feedback "negative" should be visible',
68+
).toHaveValue(details.negative);
69+
}
70+
71+
if (details.comment !== undefined) {
72+
await expect(
73+
this.page.getByRole('textbox', { name: 'Commentaire' }),
74+
'Feedback "comment" should be visible',
75+
).toHaveValue(details.comment);
76+
}
77+
}
2578

79+
async submit() {
2680
await this.page.getByRole('button', { name: 'Envoyer' }).click();
2781
await this.page.getByRole('button', { name: 'Confirmer' }).click();
2882

2983
await this.page.waitForURL('/fr/give/success');
3084
}
3185

32-
async gotoAndGive(persona: Persona, details: Details) {
86+
async gotoFillAndSubmit(persona: Persona, details: Details) {
3387
await this.goto();
34-
await this.give(persona, details);
88+
await this.fill(persona, details);
89+
await this.submit();
90+
}
91+
92+
// ----- draft -----
93+
94+
async saveAsDraft() {
95+
await this.page.getByRole('button', { name: 'Sauvegarder' }).click();
96+
}
97+
98+
async applyDraft(persona: Persona) {
99+
await this.draftHandler(persona, 'edit');
100+
}
101+
102+
async deleteDraft(persona: Persona) {
103+
await this.draftHandler(persona, 'delete');
104+
}
105+
106+
private async draftHandler(persona: Persona, action: 'edit' | 'delete') {
107+
await this.page.getByRole('button', { name: 'Brouillons' }).click();
108+
109+
// Wait until the the `<table>` is rendered
110+
await this.page.locator('tbody').waitFor();
111+
112+
await this.page
113+
.locator('tbody > tr', { has: this.page.getByRole('cell', { name: persona }) })
114+
.getByRole('button')
115+
.filter({ hasText: action })
116+
.click();
35117
}
36118
}

client/e2e/pages/manager.page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Page, expect } from '@playwright/test';
22
import { Persona } from './sign-in.page';
33

44
type Details = {
5-
message?: string; // note: there's no message for spontaneous feedback
5+
message?: string; // note: optional because there's no "message" for spontaneous feedback
66
context: string;
77
positive: string;
88
negative: string;

client/e2e/pages/user-menu.page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export class UserMenuPage {
44
constructor(private page: Page) {}
55

66
async signOut() {
7-
expect(this.page.url(), 'Should not to be in sign-in page when trying to sign-out').not.toMatch(/sign-in/);
7+
expect(this.page.url(), 'Should not be in sign-in page when trying to sign-out').not.toMatch(/sign-in/);
88

99
await this.page.getByLabel('Menu utilisateur').click();
1010
await this.page.getByRole('menuitem', { name: 'Se déconnecter' }).click();
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { expect, test } from '@playwright/test';
2+
import { FeedbackHistoryPage } from './pages/feedback-history.page';
3+
import { FirestorePage } from './pages/firestore.page';
4+
import { GiveRequestedFeedbackPage } from './pages/give-requested-feedback.page';
5+
import { RequestFeedbackPage } from './pages/request-feedback.page';
6+
import { Persona, SignInPage } from './pages/sign-in.page';
7+
import { UserMenuPage } from './pages/user-menu.page';
8+
9+
test.beforeEach(({ page }) => new FirestorePage(page).reset());
10+
11+
test('Store and draft requested feedback', async ({ page }) => {
12+
// ====== Alfred request feedback from Bernard ======
13+
14+
await new SignInPage(page).gotoAndSignIn(Persona.Alfred);
15+
16+
await new RequestFeedbackPage(page).gotoAndRequest([Persona.Bernard], 'Quel est votre feedback ?');
17+
18+
await new UserMenuPage(page).signOut();
19+
20+
// ====== Bernard replies to Alfred's request ======
21+
22+
await new SignInPage(page).gotoAndSignIn(Persona.Bernard);
23+
24+
const bernardHistoryPage = new FeedbackHistoryPage(page);
25+
await bernardHistoryPage.goto('receivedRequest');
26+
27+
const alfredReplyLink = await bernardHistoryPage.findReplyLink(Persona.Alfred);
28+
await alfredReplyLink.click();
29+
30+
// ====== Write a partial feedback (without saving it) and then reload the page ======
31+
32+
const feedbackPage = new GiveRequestedFeedbackPage(page);
33+
34+
await feedbackPage.fill({
35+
context: "J'ai travaillé avec Bernard...",
36+
});
37+
38+
await page.reload();
39+
40+
// ====== Feedback is not lost but restored from local storage ======
41+
42+
await feedbackPage.expect(Persona.Alfred, {
43+
context: "J'ai travaillé avec Bernard...",
44+
});
45+
46+
// ====== Save the feedback as a draft, write some more feedback and then reload the page ======
47+
48+
await page.getByRole('button', { name: 'Sauvegarder' }).click();
49+
50+
await feedbackPage.fill({
51+
positive: 'Ok',
52+
});
53+
54+
await page.reload();
55+
56+
// ====== Resolve the conflict between local storage and the saved draft by choosing "Restore local storage" ======
57+
58+
await page.getByRole('button', { name: 'Restaurer' }).click();
59+
60+
await feedbackPage.expect(Persona.Alfred, {
61+
context: "J'ai travaillé avec Bernard...",
62+
positive: 'Ok', // <-- It comes from the local storage
63+
});
64+
65+
// ====== Reload the page, but this time, resolve the conflict by choosing "Discard local storage" ======
66+
67+
await page.reload();
68+
69+
await page.getByRole('button', { name: 'Abandonner' }).click();
70+
71+
await feedbackPage.expect(Persona.Alfred, {
72+
context: "J'ai travaillé avec Bernard...",
73+
positive: '', // <-- It comes from the saved draft
74+
});
75+
76+
// ====== Completing and submitting the feedback ======
77+
78+
await feedbackPage.fill({
79+
positive: 'Ok',
80+
negative: 'Ko',
81+
comment: 'R.A.S',
82+
});
83+
84+
await feedbackPage.submit();
85+
86+
await expect(page.getByText(Persona.Alfred), 'Feedback receiver should be visible in the success page').toBeVisible();
87+
});

0 commit comments

Comments
 (0)