Skip to content

Commit 64deb9f

Browse files
author
Herve Tribouilloy
committed
improve Playwright coverage and add accessibility attributes
- add semantic attributes (roles, data-* hooks) to support stable selectors - extend Playwright tests for intent widget (loading, success, interaction) - fix intent readiness transitions to handle input becoming insufficient again
1 parent d65e6ca commit 64deb9f

10 files changed

Lines changed: 100 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ intentApi: {
9494
```bash
9595
npx playwright test --config=tests/playwright.dev.config.ts
9696
npx playwright test --config=tests/playwright.dev.config.ts --debug
97+
npx playwright test --config=tests/playwright.dev.config.ts -g "title is visible"
9798
```
9899

99100
------------------------------------------------------------------------

tests/intent-discovery.spec.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test.describe('Intent Discovery Widget', () => {
1212
await expect(widget).toBeVisible();
1313
});
1414

15-
test('title is visible', async () => {
15+
test('Title is visible', async () => {
1616

1717
const title = widget.getByRole('heading', {
1818
name: 'May I ask why you came here to shop?'
@@ -21,7 +21,7 @@ test.describe('Intent Discovery Widget', () => {
2121
await expect(title).toBeVisible();
2222
});
2323

24-
test('step finder cards are partially rendered', async () => {
24+
test('Step finder cards are partially rendered', async () => {
2525

2626
const subtitle = widget.locator('label.intent-subtitle', {
2727
hasText: "Describe what you're looking for"
@@ -34,7 +34,7 @@ test.describe('Intent Discovery Widget', () => {
3434
await expect(cards).toHaveCount(3);
3535
});
3636

37-
test('step finder cards toggle works', async () => {
37+
test('Step finder cards toggle works', async () => {
3838

3939
const toggle = widget.locator('.choice-tile--view-all');
4040

@@ -49,7 +49,7 @@ test.describe('Intent Discovery Widget', () => {
4949
await expect(options).not.toBeVisible();
5050
});
5151

52-
test('step finder cards are all shown when view all is clicked', async () => {
52+
test('Step finder cards are all shown when view all is clicked', async () => {
5353

5454
const toggle = widget.locator('.choice-tile--view-all');
5555

@@ -60,7 +60,7 @@ test.describe('Intent Discovery Widget', () => {
6060
await expect(cards).toHaveCount(6);
6161
});
6262

63-
test('clicking a card reveals its options', async () => {
63+
test('Clicking a card reveals its options', async () => {
6464

6565
const weatherCard = widget.locator('[data-intent-card="climate"]');
6666
const coldOption = widget.locator('[data-intent-option="Cold"]');
@@ -75,21 +75,25 @@ test.describe('Intent Discovery Widget', () => {
7575

7676
});
7777

78-
test('selecting an option activates option and card', async () => {
78+
test('Selecting an option activates option and card', async () => {
7979

8080
const weatherCard = widget.locator('[data-intent-card="climate"]');
8181
const coldOption = widget.locator('[data-intent-option="Cold"]');
8282

8383
await weatherCard.click();
8484

85+
await expect(coldOption).toHaveAttribute('data-intent-selected', 'false');
8586
await coldOption.click();
8687

88+
await weatherCard.click();
8789
await expect(coldOption).toHaveAttribute('data-intent-selected', 'true');
88-
8990
await expect(weatherCard).toHaveAttribute('data-intent-active', 'true');
9091
});
9192

92-
test('AI evaluation is triggered when option count is low', async () => {
93+
test('AI evaluation is Ready when Product Matches count is low', async ({ page }) => {
94+
95+
const suggestButton = widget.getByRole('button', { name: 'Suggest' });
96+
await expect(suggestButton).toBeDisabled();
9397

9498
const weatherCard = widget.locator('[data-intent-card="climate"]');
9599
const coldOption = widget.locator('[data-intent-option="Spring"]');
@@ -98,11 +102,45 @@ test.describe('Intent Discovery Widget', () => {
98102
await coldOption.click();
99103

100104
await expect(
101-
widget.getByText('Evaluating your preferences')
105+
widget.getByText('Ready to suggest')
102106
).toBeVisible();
103107

104-
// await widget.locator('[data-intent-card="color"]').click();
105-
// await expect(widget.locator('[data-intent-card="color"]'))
106-
// .toHaveAttribute('data-intent-active', 'true');
108+
await expect(suggestButton).toBeEnabled();
109+
await suggestButton.click();
110+
111+
const loader = widget.getByRole('status', { name: 'Loading' });
112+
await expect(loader).toBeVisible();
113+
114+
await page.waitForTimeout(3000);
115+
116+
const successBanner = widget.locator('[data-state="success"]');
117+
await expect(successBanner).toBeVisible();
118+
119+
const recommendationCard = widget.locator('[data-role="recommendation"]');
120+
await expect(recommendationCard).toBeVisible();
121+
});
122+
123+
test('AI Readiness changes when intent is interpreted', async ({ page }) => {
124+
const warningBanner = widget.locator('[data-state="warning"]');
125+
await expect(warningBanner).toBeVisible();
126+
127+
const suggestButton = widget.getByRole('button', { name: 'Suggest' });
128+
await expect(suggestButton).toBeDisabled();
129+
130+
const input = page.getByRole('textbox');
131+
132+
// become not ready if intent text length is too small
133+
await input.fill('blue running jacket');
134+
const readinessContainer = widget.locator('[data-readiness-hint]')
135+
await expect(readinessContainer).toContainText('Add 11+ characters or refine your preferences');
136+
137+
await input.fill('blue running jacket in cold weather');
138+
await expect(readinessContainer).toContainText('AI ready to interpret your request');
139+
140+
// become not ready if intent text length is too small again
141+
await input.fill('blue running jacket');
142+
await expect(readinessContainer).toContainText('Add 11+ characters or refine your preferences');
143+
144+
await expect(suggestButton).toBeEnabled();
107145
});
108146
});

vite_project/src/components/IntentDiscovery/IntentDiscoveryLayout/AttributeLayer/AttributeLayer.controller.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ export function useAttributeLayerController({
6868
const prev = prevRemaining.current;
6969
const current = intent.remainingChars;
7070

71-
if (prev !== null && prev > 0 && current <= 0) {
72-
dispatch({ type: "INTERPRETATION_READY" });
71+
if (prev !== null) {
72+
// became ready
73+
if (prev > 0 && current <= 0) {
74+
dispatch({ type: "INTERPRETATION_READY" });
75+
}
76+
77+
// became not ready again (user removed characters)
78+
if (prev <= 0 && current > 0) {
79+
dispatch({ type: "INTERPRETATION_STARTED" });
80+
}
7381
}
7482

7583
prevRemaining.current = current;

vite_project/src/components/IntentDiscovery/IntentDiscoveryLayout/AttributeLayer/IntentExplanation.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ export const IntentExplanation = ({
1919
}: IntentExplanationProps) => {
2020
const { t } = useTranslationState();
2121
const { intentState, getAiReadiness } = useIntentState()
22-
const coveragePct = getAiReadiness(attributeLayerData)
22+
const gap = getAiReadiness(attributeLayerData)
2323
const canInterpretOrSuggest =
24-
intentState.intentInterpretationReady || coveragePct === 100;
24+
intentState.intentInterpretationReady || gap === 100;
2525

2626
const intentStarted = !!intent?.text?.trim()
2727

vite_project/src/components/IntentDiscovery/IntentDiscoveryLayout/AttributeLayer/IntentExplanation/IntentReadiness.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {useIntentState} from "../../../../../state/Intent/useIntentState.ts";
33
import {NoResult} from "./IntentReadiness/NoResult.tsx";
44
import {Success} from "./IntentReadiness/Success.tsx";
55
import {Warning} from "./IntentReadiness/Warning.tsx";
6+
import {Ready} from "./IntentReadiness/Ready.tsx";
67

78
type Props = {
89
attributeLayerData: MagentoLayeredNavigation
@@ -23,10 +24,11 @@ export const IntentReadiness = ({
2324

2425
if (intentState.status === "suggestionSent") return <Success />
2526

27+
if (canInterpretOrSuggest) return <Ready attributeLayerData={attributeLayerData} />
28+
2629
return <Warning
2730
attributeLayerData={attributeLayerData}
2831
intentStarted={intentStarted}
2932
remainingChars={remainingChars}
30-
canInterpretOrSuggest={canInterpretOrSuggest}
3133
/>
3234
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type {MagentoLayeredNavigation} from "../../../../../../hooks/domain/useLayeredNavigation.tsx";
2+
import {useTranslationState} from "../../../../../../state/Translation/useTranslationState.ts";
3+
4+
type Props = {
5+
attributeLayerData: MagentoLayeredNavigation
6+
}
7+
8+
export const Ready = ({
9+
attributeLayerData
10+
}: Props) => {
11+
const {t} = useTranslationState()
12+
13+
return (
14+
<div className="intent-ai-threshold ready" data-state="warning">
15+
<div className="confidence">
16+
{t("Ready to suggest")}
17+
</div>
18+
<div className="help" data-readiness-hint>
19+
{t(`${attributeLayerData.totalCount} matches - AI ready to interpret your request`)}
20+
</div>
21+
</div>
22+
)
23+
}

vite_project/src/components/IntentDiscovery/IntentDiscoveryLayout/AttributeLayer/IntentExplanation/IntentReadiness/Success.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Success = () => {
66
const { intentState } = useIntentState()
77

88
return (
9-
<div className="intent-banner success">
9+
<div className="intent-banner success" data-state="success">
1010
{t("%s matching products found", intentState.recommendations.length)}
1111
</div>
1212
)

vite_project/src/components/IntentDiscovery/IntentDiscoveryLayout/AttributeLayer/IntentExplanation/IntentReadiness/Warning.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,18 @@ import {useIntentState} from "../../../../../../state/Intent/useIntentState.ts";
55
type Props = {
66
attributeLayerData: MagentoLayeredNavigation
77
intentStarted: boolean;
8-
canInterpretOrSuggest: boolean;
98
remainingChars: number;
109
}
1110

1211
export const Warning = ({
1312
attributeLayerData,
1413
intentStarted,
15-
canInterpretOrSuggest,
1614
remainingChars
1715
}: Props) => {
1816
const {t} = useTranslationState()
1917
const { intentState, getAiReadiness } = useIntentState()
2018
const gap = getAiReadiness(attributeLayerData)
2119

22-
const coveragePct = getAiReadiness(attributeLayerData)
23-
2420
const getAiReadinessMessage = () => {
2521
if (intentStarted) {
2622
return "Add %s+ characters or refine your preferences"
@@ -32,18 +28,12 @@ export const Warning = ({
3228
if (intentState.status === "suggestionSent" || intentState.status === "suggestionProcessing" || intentState.status === "readyToRecommend") return null;
3329

3430
return (
35-
<div className={`intent-ai-threshold ${coveragePct === 100 ? "ready" : ""}`}>
31+
<div className={`intent-ai-threshold ${gap === 100 ? "ready" : ""}`} data-state="warning">
3632
<div className="confidence">
3733
{t("Ready to suggest")}
3834
</div>
39-
<div className="help">
40-
{!canInterpretOrSuggest
41-
? t(
42-
getAiReadinessMessage(),
43-
remainingChars,
44-
gap
45-
)
46-
: t(`${attributeLayerData.totalCount} matches - AI ready to interpret your request`)}
35+
<div className="help" data-readiness-hint>
36+
{t(getAiReadinessMessage(), remainingChars, gap)}
4737
</div>
4838
</div>
4939
)

vite_project/src/components/IntentDiscovery/IntentDiscoveryLayout/ProductRecommendations/SuggestionCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {EnrichedSuggestion} from "../../../../types/infra/magento/product.t
44

55
export const SuggestionCard: React.FC<{ suggestion: EnrichedSuggestion }> = ({ suggestion }) => {
66
const content = (
7-
<>
7+
<div data-role="recommendation">
88
{suggestion.product.imageUrl && (
99
<img
1010
src={suggestion.product.imageUrl}
@@ -47,7 +47,7 @@ export const SuggestionCard: React.FC<{ suggestion: EnrichedSuggestion }> = ({ s
4747
{suggestion.reason}
4848
</div>
4949
</div>
50-
</>
50+
</div>
5151
)
5252

5353
return suggestion.product.url ? (

vite_project/src/state/Intent/IntentStateProvider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export const IntentStateProvider: React.FC<IntentStateProviderProps> = ({ childr
5151

5252
function transition(state: IntentEngineState, event: IntentEvent): IntentEngineState {
5353
switch (event.type) {
54+
case "INTERPRETATION_STARTED":
55+
return { ...state,
56+
intentInterpretationReady: false,
57+
status: "idle"
58+
};
5459
case "INTERPRETATION_READY":
5560
return { ...state,
5661
intentInterpretationReady: true,

0 commit comments

Comments
 (0)