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 : / t r a v e l .* p l a n n e r | s u b g r a p h s / i } ) ;
41+ this . agentGreeting = page . getByText ( / t r a v e l .* p l a n n i n g | s u p e r v i s o r .* c o o r d i n a t e / 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 ( / K L M .* \$ 6 5 0 .* 1 1 h 3 0 m / ) ;
50+ this . unitedFlightOption = page . getByText ( / U n i t e d .* \$ 7 2 0 .* 1 2 h 1 5 m / ) ;
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 ( / H o t e l Z e p h y r .* F i s h e r m a n \' s W h a r f .* \$ 2 8 0 / ) ;
56+ this . ritzCarltonOption = page . getByText ( / R i t z - C a r l t o n .* N o b H i l l .* \$ 5 5 0 / ) ;
57+ this . hotelZoeOption = page . getByText ( / H o t e l Z o e .* U n i o n S q u a r e .* \$ 3 2 0 / ) ;
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 ( / f l i g h t .* o p t i o n s | A m s t e r d a m .* S a n F r a n c i s c o | K L M | U n i t e d / 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 ( / h o t e l .* o p t i o n s | a c c o m m o d a t i o n | Z e p h y r | R i t z - C a r l t o n | H o t e l Z o e / 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 ( / e x p e r i e n c e | a c t i v i t i e s | r e s t a u r a n t | P i e r 3 9 | G o l d e n G a t e | S w a n O y s t e r | T a r t i n e / 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 ( / K L M .* \$ 6 5 0 .* 1 1 h 3 0 m / ) . first ( ) ) . toBeVisible ( ) ;
143+ await expect ( this . page . getByText ( / U n i t e d .* \$ 7 2 0 .* 1 2 h 1 5 m / ) . first ( ) ) . toBeVisible ( ) ;
144+ }
145+
146+ async verifyStaticHotelData ( ) {
147+ // Verify the hardcoded hotel options are present
148+ await expect ( this . page . getByText ( / H o t e l Z e p h y r .* \$ 2 8 0 / ) . first ( ) ) . toBeVisible ( ) ;
149+ await expect ( this . page . getByText ( / R i t z - C a r l t o n .* \$ 5 5 0 / ) . first ( ) ) . toBeVisible ( ) ;
150+ await expect ( this . page . getByText ( / H o t e l Z o e .* \$ 3 2 0 / ) . 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 ( / P i e r 3 9 | G o l d e n G a t e B r i d g e | S w a n O y s t e r D e p o t | T a r t i n e B a k e r y / 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 ( / s u p e r v i s o r | c o o r d i n a t e | s p e c i a l i s t | r o u t i n g / 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 ( / c o m p l e t e | f i n i s h e d | p l a n n i n g .* d o n e | i t i n e r a r y .* r e a d y / i) . first ( )
200+ ) . toBeVisible ( { timeout : 30000 } ) ;
201+ }
202+ }
0 commit comments