Skip to content

Commit ce23885

Browse files
Add UI testing using PlayWright
1 parent 3b3d02b commit ce23885

5 files changed

Lines changed: 199 additions & 1 deletion

File tree

ng-testapp/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,17 @@
4646
<artifactId>gson</artifactId>
4747
<version>2.13.2</version>
4848
</dependency>
49+
<dependency>
50+
<groupId>org.junit.jupiter</groupId>
51+
<artifactId>junit-jupiter-engine</artifactId>
52+
<version>6.0.3</version>
53+
<scope>test</scope>
54+
</dependency>
55+
<dependency>
56+
<groupId>com.microsoft.playwright</groupId>
57+
<artifactId>playwright</artifactId>
58+
<version>1.50.0</version>
59+
<scope>test</scope>
60+
</dependency>
4961
</dependencies>
5062
</project>

ng-testapp/src/main/java/ng/testapp/Application.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ng.appserver.directactions.NGDirectActionRequestHandler;
88
import ng.plugins.Elements;
99
import ng.plugins.Routes;
10+
import ng.testapp.components.TAAjaxPage;
1011
import ng.testapp.components.TAMain;
1112
import ng.testapp.components.TAProgrammaticDynamicComponent;
1213
import ng.testapp.components.TASingleFileComponent;
@@ -81,7 +82,10 @@ public Routes routes() {
8182

8283
// Testing programmatic component generation
8384
// FIXME: Doesn't really belong in the demo project, find a new home // Hugi 2025-09-27
84-
.map( "/component-programmatic", TAProgrammaticDynamicComponent.class );
85+
.map( "/component-programmatic", TAProgrammaticDynamicComponent.class )
86+
87+
// Ajax test page
88+
.map( "/ajax", TAAjaxPage.class );
8589
}
8690

8791
private NGActionResults stringReponse() {

ng-testapp/src/main/java/ng/testapp/components/TAAjaxPage.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
public class TAAjaxPage extends NGComponent {
1212

1313
public String myName;
14+
public String observedValue = "";
15+
public String containerFieldA = "";
16+
public String containerFieldB = "";
1417

1518
public TAAjaxPage( NGContext context ) {
1619
super( context );
@@ -22,6 +25,14 @@ public String currentTime() {
2225
return "The current time is %s. %s".formatted( dateTime, encouragingMessage );
2326
}
2427

28+
public String observedEcho() {
29+
return "Echo: " + observedValue;
30+
}
31+
32+
public String containerEcho() {
33+
return "A=" + containerFieldA + ", B=" + containerFieldB;
34+
}
35+
2536
private List<String> encouragingMessages() {
2637
return List.of( "Your eyes look amazing!",
2738
"Your clicks rock my world!",

ng-testapp/src/main/resources/components/TAAjaxPage.wo/TAAjaxPage.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,33 @@ <h5>Container 2</h5>
4242

4343
<hr style="margin: 50px" />
4444

45+
<h5>Single-field observe (AjaxObserveField)</h5>
46+
<wo:form>
47+
<label>Type something:</label>
48+
<wo:textfield value="$observedValue" id="observeInput" />
49+
<wo:AjaxObserveField observeFieldID="observeInput" updateContainerID="observeUC" fullSubmit="$true" />
50+
</wo:form>
51+
<wo:AjaxUpdateContainer id="observeUC" class="card mt-3 mb-3 p-3">
52+
<span id="observeEcho"><wo:str value="$observedEcho" /></span>
53+
</wo:AjaxUpdateContainer>
54+
55+
<hr style="margin: 50px" />
56+
57+
<h5>Container-mode observe (AjaxObserveField wrapping descendants)</h5>
58+
<wo:form>
59+
<wo:AjaxObserveField updateContainerID="containerUC" fullSubmit="$true">
60+
<label>Field A:</label>
61+
<wo:textfield value="$containerFieldA" id="containerFieldA" />
62+
<label>Field B:</label>
63+
<wo:textfield value="$containerFieldB" id="containerFieldB" />
64+
</wo:AjaxObserveField>
65+
</wo:form>
66+
<wo:AjaxUpdateContainer id="containerUC" class="card mt-3 mb-3 p-3">
67+
<span id="containerEcho"><wo:str value="$containerEcho" /></span>
68+
</wo:AjaxUpdateContainer>
69+
70+
<hr style="margin: 50px" />
71+
4572
<h5>Submit a form using Ajax</h5>
4673
<p>Note that pushing "return" will just submit the page. You need to click the button. Yeah. Work in progress.</p>
4774

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package ng.testapp;
2+
3+
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
4+
5+
import org.junit.jupiter.api.AfterAll;
6+
import org.junit.jupiter.api.AfterEach;
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
import com.microsoft.playwright.Browser;
12+
import com.microsoft.playwright.BrowserContext;
13+
import com.microsoft.playwright.Page;
14+
import com.microsoft.playwright.Playwright;
15+
16+
import ng.appserver.NGApplication;
17+
18+
/**
19+
* Playwright-based integration tests for the framework's client-side Ajax support.
20+
*
21+
* Boots the ng-testapp application on port 1200, then uses a headless browser
22+
* to exercise AjaxUpdateLink, AjaxUpdateContainer, AjaxObserveField and friends.
23+
*/
24+
public class TestAjax {
25+
26+
private static final String BASE_URL = "http://localhost:1200";
27+
28+
// Shared across all tests — expensive to create
29+
static Playwright playwright;
30+
static Browser browser;
31+
32+
// Fresh per test — full isolation (cookies, storage, etc.)
33+
BrowserContext context;
34+
Page page;
35+
36+
@BeforeAll
37+
static void startAppAndBrowser() {
38+
// Boot the ng-testapp server (blocks until Jetty is listening)
39+
NGApplication.runAndReturn( new String[0], Application.class );
40+
41+
playwright = Playwright.create();
42+
browser = playwright.chromium().launch();
43+
}
44+
45+
@AfterAll
46+
static void closeBrowser() {
47+
if( playwright != null ) {
48+
playwright.close();
49+
}
50+
}
51+
52+
@BeforeEach
53+
void createPage() {
54+
context = browser.newContext();
55+
page = context.newPage();
56+
}
57+
58+
@AfterEach
59+
void closePage() {
60+
context.close();
61+
}
62+
63+
// --- Basic page load -----------------------------------------------------
64+
65+
@Test
66+
void ajaxPageLoads() {
67+
page.navigate( BASE_URL + "/ajax" );
68+
assertThat( page ).hasTitle( "Welcome to ng" );
69+
assertThat( page.getByText( "Partial page updates (Ajax)" ) ).isVisible();
70+
}
71+
72+
// --- AjaxUpdateLink + AjaxUpdateContainer --------------------------------
73+
74+
@Test
75+
void ajaxUpdateLinkUpdatesContainer() {
76+
page.navigate( BASE_URL + "/ajax" );
77+
78+
// Click the "Show current time" link
79+
page.getByText( "Show current time" ).click();
80+
81+
// The container should now contain a time string
82+
assertThat( page.locator( "#timeUC" ) ).containsText( "The current time is" );
83+
}
84+
85+
@Test
86+
void ajaxUpdateLinkUpdatesMultipleContainers() {
87+
page.navigate( BASE_URL + "/ajax" );
88+
89+
// Click the "Update both" link
90+
page.getByText( "Update both" ).click();
91+
92+
// Both containers should now contain time strings
93+
assertThat( page.locator( "#uc1" ) ).containsText( "The current time is" );
94+
assertThat( page.locator( "#uc2" ) ).containsText( "The current time is" );
95+
}
96+
97+
// --- AjaxSubmitButton ----------------------------------------------------
98+
99+
@Test
100+
void ajaxSubmitButtonSubmitsFormAndUpdatesContainer() {
101+
page.navigate( BASE_URL + "/ajax" );
102+
103+
// NGTextField renders as a plain <input type="text"> without an id.
104+
// The "Say hello" button is in the same form, so locate the text field relative to it.
105+
final var submitButton = page.locator( "input[value='Say hello']" );
106+
submitButton.locator( "xpath=preceding-sibling::input[@type='text']" ).fill( "Playwright" );
107+
submitButton.click();
108+
109+
// The container should greet us
110+
assertThat( page.locator( "#helloUC" ) ).containsText( "Hello Playwright" );
111+
}
112+
113+
// --- AjaxObserveField (single-field mode) --------------------------------
114+
115+
@Test
116+
void observeFieldSingleModeEchosInput() {
117+
page.navigate( BASE_URL + "/ajax" );
118+
119+
// Type into the observed input field
120+
page.locator( "#observeInput" ).fill( "hello world" );
121+
122+
// The echo container should update after debounce (default 300ms)
123+
assertThat( page.locator( "#observeEcho" ) ).containsText( "Echo: hello world" );
124+
}
125+
126+
// --- AjaxObserveField (container mode) -----------------------------------
127+
128+
@Test
129+
void observeFieldContainerModeObservesDescendants() {
130+
page.navigate( BASE_URL + "/ajax" );
131+
132+
// Type into Field A
133+
page.locator( "#containerFieldA" ).fill( "alpha" );
134+
135+
// The container echo should reflect Field A's value
136+
assertThat( page.locator( "#containerEcho" ) ).containsText( "A=alpha" );
137+
138+
// Now type into Field B
139+
page.locator( "#containerFieldB" ).fill( "beta" );
140+
141+
// The container echo should reflect both values
142+
assertThat( page.locator( "#containerEcho" ) ).containsText( "A=alpha, B=beta" );
143+
}
144+
}

0 commit comments

Comments
 (0)