Skip to content

Commit 289ef27

Browse files
committed
chore: add e2e tests for subgraphs feature
1 parent 7d05faa commit 289ef27

File tree

6 files changed

+698
-9
lines changed

6 files changed

+698
-9
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { Page, Locator, expect } from '@playwright/test';
2+
3+
export class SubgraphsPage {
4+
readonly page: Page;
5+
readonly travelPlannerButton: Locator;
6+
readonly chatInput: Locator;
7+
readonly sendButton: Locator;
8+
readonly agentGreeting: Locator;
9+
readonly agentMessage: Locator;
10+
readonly userMessage: Locator;
11+
12+
// Flight-related elements
13+
readonly flightOptions: Locator;
14+
readonly klmFlightOption: Locator;
15+
readonly unitedFlightOption: Locator;
16+
readonly flightSelectionInterface: Locator;
17+
18+
// Hotel-related elements
19+
readonly hotelOptions: Locator;
20+
readonly hotelZephyrOption: Locator;
21+
readonly ritzCarltonOption: Locator;
22+
readonly hotelZoeOption: Locator;
23+
readonly hotelSelectionInterface: Locator;
24+
25+
// Itinerary and state elements
26+
readonly itineraryDisplay: Locator;
27+
readonly selectedFlight: Locator;
28+
readonly selectedHotel: Locator;
29+
readonly experienceRecommendations: Locator;
30+
31+
// Subgraph activity indicators
32+
readonly activeAgent: Locator;
33+
readonly supervisorIndicator: Locator;
34+
readonly flightsAgentIndicator: Locator;
35+
readonly hotelsAgentIndicator: Locator;
36+
readonly experiencesAgentIndicator: Locator;
37+
38+
constructor(page: Page) {
39+
this.page = page;
40+
this.travelPlannerButton = page.getByRole('button', { name: /travel.*planner|subgraphs/i });
41+
this.agentGreeting = page.getByText(/travel.*planning|supervisor.*coordinate/i);
42+
this.chatInput = page.getByRole('textbox', { name: 'Type a message...' });
43+
this.sendButton = page.locator('[data-test-id="copilot-chat-ready"]');
44+
this.agentMessage = page.locator('.copilotKitAssistantMessage');
45+
this.userMessage = page.locator('.copilotKitUserMessage');
46+
47+
// Flight selection elements
48+
this.flightOptions = page.locator('[data-testid*="flight"], .flight-option');
49+
this.klmFlightOption = page.getByText(/KLM.*\$650.*11h 30m/);
50+
this.unitedFlightOption = page.getByText(/United.*\$720.*12h 15m/);
51+
this.flightSelectionInterface = page.locator('[data-testid*="flight-select"], .flight-selection');
52+
53+
// Hotel selection elements
54+
this.hotelOptions = page.locator('[data-testid*="hotel"], .hotel-option');
55+
this.hotelZephyrOption = page.getByText(/Hotel Zephyr.*Fisherman\'s Wharf.*\$280/);
56+
this.ritzCarltonOption = page.getByText(/Ritz-Carlton.*Nob Hill.*\$550/);
57+
this.hotelZoeOption = page.getByText(/Hotel Zoe.*Union Square.*\$320/);
58+
this.hotelSelectionInterface = page.locator('[data-testid*="hotel-select"], .hotel-selection');
59+
60+
// Itinerary elements
61+
this.itineraryDisplay = page.locator('[data-testid*="itinerary"], .itinerary');
62+
this.selectedFlight = page.locator('[data-testid*="selected-flight"], .selected-flight');
63+
this.selectedHotel = page.locator('[data-testid*="selected-hotel"], .selected-hotel');
64+
this.experienceRecommendations = page.locator('[data-testid*="experience"], .experience');
65+
66+
// Agent activity indicators
67+
this.activeAgent = page.locator('[data-testid*="active-agent"], .active-agent');
68+
this.supervisorIndicator = page.locator('[data-testid*="supervisor"], .supervisor-active');
69+
this.flightsAgentIndicator = page.locator('[data-testid*="flights-agent"], .flights-agent-active');
70+
this.hotelsAgentIndicator = page.locator('[data-testid*="hotels-agent"], .hotels-agent-active');
71+
this.experiencesAgentIndicator = page.locator('[data-testid*="experiences-agent"], .experiences-agent-active');
72+
}
73+
74+
async openChat() {
75+
await this.travelPlannerButton.click();
76+
}
77+
78+
async sendMessage(message: string) {
79+
await this.chatInput.click();
80+
await this.chatInput.fill(message);
81+
await this.sendButton.click();
82+
}
83+
84+
async selectFlight(airline: 'KLM' | 'United') {
85+
const flightOption = airline === 'KLM' ? this.klmFlightOption : this.unitedFlightOption;
86+
87+
// Wait for flight options to be presented
88+
await expect(this.flightOptions.first()).toBeVisible({ timeout: 15000 });
89+
90+
// Click on the desired flight option
91+
await flightOption.click();
92+
}
93+
94+
async selectHotel(hotel: 'Zephyr' | 'Ritz-Carlton' | 'Zoe') {
95+
let hotelOption: Locator;
96+
97+
switch (hotel) {
98+
case 'Zephyr':
99+
hotelOption = this.hotelZephyrOption;
100+
break;
101+
case 'Ritz-Carlton':
102+
hotelOption = this.ritzCarltonOption;
103+
break;
104+
case 'Zoe':
105+
hotelOption = this.hotelZoeOption;
106+
break;
107+
}
108+
109+
// Wait for hotel options to be presented
110+
await expect(this.hotelOptions.first()).toBeVisible({ timeout: 15000 });
111+
112+
// Click on the desired hotel option
113+
await hotelOption.click();
114+
}
115+
116+
async waitForFlightsAgent() {
117+
// Wait for flights agent to become active (or look for flight-related content)
118+
// Use .first() to handle multiple matches in strict mode
119+
await expect(
120+
this.page.getByText(/flight.*options|Amsterdam.*San Francisco|KLM|United/i).first()
121+
).toBeVisible({ timeout: 20000 });
122+
}
123+
124+
async waitForHotelsAgent() {
125+
// Wait for hotels agent to become active (or look for hotel-related content)
126+
// Use .first() to handle multiple matches in strict mode
127+
await expect(
128+
this.page.getByText(/hotel.*options|accommodation|Zephyr|Ritz-Carlton|Hotel Zoe/i).first()
129+
).toBeVisible({ timeout: 20000 });
130+
}
131+
132+
async waitForExperiencesAgent() {
133+
// Wait for experiences agent to become active (or look for experience-related content)
134+
// Use .first() to handle multiple matches in strict mode
135+
await expect(
136+
this.page.getByText(/experience|activities|restaurant|Pier 39|Golden Gate|Swan Oyster|Tartine/i).first()
137+
).toBeVisible({ timeout: 20000 });
138+
}
139+
140+
async verifyStaticFlightData() {
141+
// Verify the hardcoded flight options are present
142+
await expect(this.page.getByText(/KLM.*\$650.*11h 30m/).first()).toBeVisible();
143+
await expect(this.page.getByText(/United.*\$720.*12h 15m/).first()).toBeVisible();
144+
}
145+
146+
async verifyStaticHotelData() {
147+
// Verify the hardcoded hotel options are present
148+
await expect(this.page.getByText(/Hotel Zephyr.*\$280/).first()).toBeVisible();
149+
await expect(this.page.getByText(/Ritz-Carlton.*\$550/).first()).toBeVisible();
150+
await expect(this.page.getByText(/Hotel Zoe.*\$320/).first()).toBeVisible();
151+
}
152+
153+
async verifyStaticExperienceData() {
154+
// Verify the hardcoded experience options are mentioned
155+
const experienceText = this.page.getByText(/Pier 39|Golden Gate Bridge|Swan Oyster Depot|Tartine Bakery/i);
156+
await expect(experienceText.first()).toBeVisible();
157+
}
158+
159+
async verifyItineraryContainsFlight(airline: 'KLM' | 'United') {
160+
// Check that the selected flight appears in the itinerary or conversation
161+
await expect(this.page.getByText(new RegExp(airline, 'i'))).toBeVisible();
162+
}
163+
164+
async verifyItineraryContainsHotel(hotel: 'Zephyr' | 'Ritz-Carlton' | 'Zoe') {
165+
// Check that the selected hotel appears in the itinerary or conversation
166+
const hotelName = hotel === 'Ritz-Carlton' ? 'Ritz-Carlton' : `Hotel ${hotel}`;
167+
await expect(this.page.getByText(new RegExp(hotelName, 'i'))).toBeVisible();
168+
}
169+
170+
async assertAgentReplyVisible(expectedText: RegExp) {
171+
await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible();
172+
}
173+
174+
async assertUserMessageVisible(message: string) {
175+
await expect(this.page.getByText(message)).toBeVisible();
176+
}
177+
178+
async waitForSupervisorCoordination() {
179+
// Wait for supervisor to appear in the conversation
180+
await expect(
181+
this.page.getByText(/supervisor|coordinate|specialist|routing/i).first()
182+
).toBeVisible({ timeout: 15000 });
183+
}
184+
185+
async waitForAgentCompletion() {
186+
// Wait for the travel planning process to complete
187+
await expect(
188+
this.page.getByText(/complete|finished|planning.*done|itinerary.*ready/i).first()
189+
).toBeVisible({ timeout: 30000 });
190+
}
191+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { Page, Locator, expect } from '@playwright/test';
2+
3+
export class SubgraphsPage {
4+
readonly page: Page;
5+
readonly travelPlannerButton: Locator;
6+
readonly chatInput: Locator;
7+
readonly sendButton: Locator;
8+
readonly agentGreeting: Locator;
9+
readonly agentMessage: Locator;
10+
readonly userMessage: Locator;
11+
12+
// Flight-related elements
13+
readonly flightOptions: Locator;
14+
readonly klmFlightOption: Locator;
15+
readonly unitedFlightOption: Locator;
16+
readonly flightSelectionInterface: Locator;
17+
18+
// Hotel-related elements
19+
readonly hotelOptions: Locator;
20+
readonly hotelZephyrOption: Locator;
21+
readonly ritzCarltonOption: Locator;
22+
readonly hotelZoeOption: Locator;
23+
readonly hotelSelectionInterface: Locator;
24+
25+
// Itinerary and state elements
26+
readonly itineraryDisplay: Locator;
27+
readonly selectedFlight: Locator;
28+
readonly selectedHotel: Locator;
29+
readonly experienceRecommendations: Locator;
30+
31+
// Subgraph activity indicators
32+
readonly activeAgent: Locator;
33+
readonly supervisorIndicator: Locator;
34+
readonly flightsAgentIndicator: Locator;
35+
readonly hotelsAgentIndicator: Locator;
36+
readonly experiencesAgentIndicator: Locator;
37+
38+
constructor(page: Page) {
39+
this.page = page;
40+
this.travelPlannerButton = page.getByRole('button', { name: /travel.*planner|subgraphs/i });
41+
this.agentGreeting = page.getByText(/travel.*planning|supervisor.*coordinate/i);
42+
this.chatInput = page.getByRole('textbox', { name: 'Type a message...' });
43+
this.sendButton = page.locator('[data-test-id="copilot-chat-ready"]');
44+
this.agentMessage = page.locator('.copilotKitAssistantMessage');
45+
this.userMessage = page.locator('.copilotKitUserMessage');
46+
47+
// Flight selection elements
48+
this.flightOptions = page.locator('[data-testid*="flight"], .flight-option');
49+
this.klmFlightOption = page.getByText(/KLM.*\$650.*11h 30m/);
50+
this.unitedFlightOption = page.getByText(/United.*\$720.*12h 15m/);
51+
this.flightSelectionInterface = page.locator('[data-testid*="flight-select"], .flight-selection');
52+
53+
// Hotel selection elements
54+
this.hotelOptions = page.locator('[data-testid*="hotel"], .hotel-option');
55+
this.hotelZephyrOption = page.getByText(/Hotel Zephyr.*Fisherman\'s Wharf.*\$280/);
56+
this.ritzCarltonOption = page.getByText(/Ritz-Carlton.*Nob Hill.*\$550/);
57+
this.hotelZoeOption = page.getByText(/Hotel Zoe.*Union Square.*\$320/);
58+
this.hotelSelectionInterface = page.locator('[data-testid*="hotel-select"], .hotel-selection');
59+
60+
// Itinerary elements
61+
this.itineraryDisplay = page.locator('[data-testid*="itinerary"], .itinerary');
62+
this.selectedFlight = page.locator('[data-testid*="selected-flight"], .selected-flight');
63+
this.selectedHotel = page.locator('[data-testid*="selected-hotel"], .selected-hotel');
64+
this.experienceRecommendations = page.locator('[data-testid*="experience"], .experience');
65+
66+
// Agent activity indicators
67+
this.activeAgent = page.locator('[data-testid*="active-agent"], .active-agent');
68+
this.supervisorIndicator = page.locator('[data-testid*="supervisor"], .supervisor-active');
69+
this.flightsAgentIndicator = page.locator('[data-testid*="flights-agent"], .flights-agent-active');
70+
this.hotelsAgentIndicator = page.locator('[data-testid*="hotels-agent"], .hotels-agent-active');
71+
this.experiencesAgentIndicator = page.locator('[data-testid*="experiences-agent"], .experiences-agent-active');
72+
}
73+
74+
async openChat() {
75+
await this.travelPlannerButton.click();
76+
}
77+
78+
async sendMessage(message: string) {
79+
await this.chatInput.click();
80+
await this.chatInput.fill(message);
81+
await this.sendButton.click();
82+
}
83+
84+
async selectFlight(airline: 'KLM' | 'United') {
85+
const flightOption = airline === 'KLM' ? this.klmFlightOption : this.unitedFlightOption;
86+
87+
// Wait for flight options to be presented
88+
await expect(this.flightOptions.first()).toBeVisible({ timeout: 15000 });
89+
90+
// Click on the desired flight option
91+
await flightOption.click();
92+
}
93+
94+
async selectHotel(hotel: 'Zephyr' | 'Ritz-Carlton' | 'Zoe') {
95+
let hotelOption: Locator;
96+
97+
switch (hotel) {
98+
case 'Zephyr':
99+
hotelOption = this.hotelZephyrOption;
100+
break;
101+
case 'Ritz-Carlton':
102+
hotelOption = this.ritzCarltonOption;
103+
break;
104+
case 'Zoe':
105+
hotelOption = this.hotelZoeOption;
106+
break;
107+
}
108+
109+
// Wait for hotel options to be presented
110+
await expect(this.hotelOptions.first()).toBeVisible({ timeout: 15000 });
111+
112+
// Click on the desired hotel option
113+
await hotelOption.click();
114+
}
115+
116+
async waitForFlightsAgent() {
117+
// Wait for flights agent to become active (or look for flight-related content)
118+
// Use .first() to handle multiple matches in strict mode
119+
await expect(
120+
this.page.getByText(/flight.*options|Amsterdam.*San Francisco|KLM|United/i).first()
121+
).toBeVisible({ timeout: 20000 });
122+
}
123+
124+
async waitForHotelsAgent() {
125+
// Wait for hotels agent to become active (or look for hotel-related content)
126+
// Use .first() to handle multiple matches in strict mode
127+
await expect(
128+
this.page.getByText(/hotel.*options|accommodation|Zephyr|Ritz-Carlton|Hotel Zoe/i).first()
129+
).toBeVisible({ timeout: 20000 });
130+
}
131+
132+
async waitForExperiencesAgent() {
133+
// Wait for experiences agent to become active (or look for experience-related content)
134+
// Use .first() to handle multiple matches in strict mode
135+
await expect(
136+
this.page.getByText(/experience|activities|restaurant|Pier 39|Golden Gate|Swan Oyster|Tartine/i).first()
137+
).toBeVisible({ timeout: 20000 });
138+
}
139+
140+
async verifyStaticFlightData() {
141+
// Verify the hardcoded flight options are present
142+
await expect(this.page.getByText(/KLM.*\$650.*11h 30m/).first()).toBeVisible();
143+
await expect(this.page.getByText(/United.*\$720.*12h 15m/).first()).toBeVisible();
144+
}
145+
146+
async verifyStaticHotelData() {
147+
// Verify the hardcoded hotel options are present
148+
await expect(this.page.getByText(/Hotel Zephyr.*\$280/).first()).toBeVisible();
149+
await expect(this.page.getByText(/Ritz-Carlton.*\$550/).first()).toBeVisible();
150+
await expect(this.page.getByText(/Hotel Zoe.*\$320/).first()).toBeVisible();
151+
}
152+
153+
async verifyStaticExperienceData() {
154+
// Wait for experiences to load - this can take time as it's the final step in the agent flow
155+
// First ensure we're not stuck in "No experiences planned yet" state
156+
await expect(this.page.getByText('No experiences planned yet')).not.toBeVisible({ timeout: 20000 }).catch(() => {
157+
console.log('Still waiting for experiences to load...');
158+
});
159+
160+
// Wait for actual experience content to appear
161+
await expect(this.page.locator('.activity-name').first()).toBeVisible({ timeout: 15000 });
162+
163+
// Verify we have meaningful experience content (either static or AI-generated)
164+
const experienceContent = this.page.locator('.activity-name').first().or(
165+
this.page.getByText(/Pier 39|Golden Gate Bridge|Swan Oyster Depot|Tartine Bakery/i).first()
166+
);
167+
await expect(experienceContent).toBeVisible();
168+
}
169+
170+
async verifyItineraryContainsFlight(airline: 'KLM' | 'United') {
171+
// Check that the selected flight appears in the itinerary or conversation
172+
await expect(this.page.getByText(new RegExp(airline, 'i'))).toBeVisible();
173+
}
174+
175+
async verifyItineraryContainsHotel(hotel: 'Zephyr' | 'Ritz-Carlton' | 'Zoe') {
176+
// Check that the selected hotel appears in the itinerary or conversation
177+
const hotelName = hotel === 'Ritz-Carlton' ? 'Ritz-Carlton' : `Hotel ${hotel}`;
178+
await expect(this.page.getByText(new RegExp(hotelName, 'i'))).toBeVisible();
179+
}
180+
181+
async assertAgentReplyVisible(expectedText: RegExp) {
182+
await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible();
183+
}
184+
185+
async assertUserMessageVisible(message: string) {
186+
await expect(this.page.getByText(message)).toBeVisible();
187+
}
188+
189+
async waitForSupervisorCoordination() {
190+
// Wait for supervisor to appear in the conversation
191+
await expect(
192+
this.page.getByText(/supervisor|coordinate|specialist|routing/i).first()
193+
).toBeVisible({ timeout: 15000 });
194+
}
195+
196+
async waitForAgentCompletion() {
197+
// Wait for the travel planning process to complete
198+
await expect(
199+
this.page.getByText(/complete|finished|planning.*done|itinerary.*ready/i).first()
200+
).toBeVisible({ timeout: 30000 });
201+
}
202+
}

0 commit comments

Comments
 (0)