For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Implement E2E GUI tests for JPhotoTagger's Import, Keyword Tagging, and Search workflows using AssertJ Swing with Page Object pattern.
Architecture: Page Objects wrap AssertJ Swing interactions. Tests launch the real app via AppInit, interact with UI components, and verify state changes. Test data uses embedded placeholder JPGs copied to temp directories. CI runs tests via Xvfb.
Tech Stack: AssertJ Swing 3.17.1, JUnit 5, Xvfb for CI
Files:
gradle/libs.versions.tomlProgram/build.gradle.ktsStep 1: Add AssertJ Swing to version catalog
Edit gradle/libs.versions.toml, add after line 17 (jmh = "1.37"):
assertj-swing = "3.17.1"
Add after line 39 (jmh-generator = ...):
assertj-swing = { module = "org.assertj:assertj-swing-junit", version.ref = "assertj-swing" }
Step 2: Add dependency to Program module
Edit Program/build.gradle.kts, add after line 66 (testImplementation(project(":TestSupport"))):
testImplementation(libs.assertj.swing)
testImplementation(libs.bundles.junit5)
testImplementation(libs.assertj)
Step 3: Add e2eTest task to Program module
Edit Program/build.gradle.kts, add after line 95 (}):
tasks.register<Test>("e2eTest") {
description = "Runs E2E GUI tests"
group = "verification"
useJUnitPlatform {
includeTags("e2e")
}
failFast = true
// E2E tests need more memory for GUI
maxHeapSize = "512m"
}
Step 4: Verify build still works
Run: ./gradlew :Program:dependencies --configuration testCompileClasspath | grep assertj
Expected: Output contains assertj-swing and assertj-core
Step 5: Commit
git add gradle/libs.versions.toml Program/build.gradle.kts
git commit -m "build: add AssertJ Swing for E2E GUI testing"
Files:
Program/src/test/resources/e2e/photos/test-photo-01.jpgProgram/src/test/resources/e2e/photos/test-photo-02.jpgProgram/src/test/resources/e2e/photos/test-photo-03.jpgProgram/src/test/resources/e2e/photos/README.mdStep 1: Create directories
Run: mkdir -p Program/src/test/resources/e2e/photos
Step 2: Create minimal valid JPEG files
These are 1x1 pixel valid JPEGs. Create using Java (faster than external tools):
Create file Program/src/test/resources/e2e/photos/generate-test-photos.sh:
#!/bin/bash
# Generate minimal test JPEG files using ImageMagick or Java
cd "$(dirname "$0")"
# If ImageMagick is available
if command -v convert &> /dev/null; then
convert -size 100x100 xc:red test-photo-01.jpg
convert -size 100x100 xc:green test-photo-02.jpg
convert -size 100x100 xc:blue test-photo-03.jpg
echo "Created test photos with ImageMagick"
exit 0
fi
echo "ImageMagick not found. Creating test photos with base64-encoded minimal JPEG."
# Minimal 1x1 red JPEG (base64 encoded)
echo "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBEQCEAwEPwAB/9k=" | base64 -d > test-photo-01.jpg
echo "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBEQCEAwEPwAB/9k=" | base64 -d > test-photo-02.jpg
echo "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBEQCEAwEPwAB/9k=" | base64 -d > test-photo-03.jpg
echo "Created test photos with base64"
Run: chmod +x Program/src/test/resources/e2e/photos/generate-test-photos.sh && Program/src/test/resources/e2e/photos/generate-test-photos.sh
Step 3: Create README for test photos
Create file Program/src/test/resources/e2e/photos/README.md:
# E2E Test Photos
Minimal placeholder JPEG images for E2E GUI testing.
These are 1x1 pixel images used to test the import workflow without
requiring real photo files. They are valid JPEG files that JPhotoTagger
can process.
## Files
- `test-photo-01.jpg` - Red placeholder
- `test-photo-02.jpg` - Green placeholder
- `test-photo-03.jpg` - Blue placeholder
## Regenerating
Run `./generate-test-photos.sh` to regenerate these files.
Step 4: Verify files exist
Run: ls -la Program/src/test/resources/e2e/photos/
Expected: Three .jpg files and README.md present
Step 5: Commit
git add Program/src/test/resources/e2e/
git commit -m "test: add placeholder photos for E2E tests"
Files:
Program/src/test/java/org/jphototagger/e2e/base/TestDataManager.javaStep 1: Create directory structure
Run: mkdir -p Program/src/test/java/org/jphototagger/e2e/base
Step 2: Create TestDataManager
Create file Program/src/test/java/org/jphototagger/e2e/base/TestDataManager.java:
package org.jphototagger.e2e.base;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
/**
* Manages test photo files for E2E tests.
* Creates temp directories, copies test photos from resources,
* and cleans up after tests.
*/
public class TestDataManager {
private static final String[] TEST_PHOTOS = {
"test-photo-01.jpg",
"test-photo-02.jpg",
"test-photo-03.jpg"
};
private Path tempDirectory;
/**
* Creates a new temporary directory for this test.
*/
public void createTempDirectory() throws IOException {
tempDirectory = Files.createTempDirectory("jphototagger-e2e-");
}
/**
* Copies test photos from resources to the temp directory.
*/
public void copyTestPhotos() throws IOException {
if (tempDirectory == null) {
throw new IllegalStateException("Call createTempDirectory() first");
}
for (String photoName : TEST_PHOTOS) {
String resourcePath = "/e2e/photos/" + photoName;
try (InputStream is = getClass().getResourceAsStream(resourcePath)) {
if (is == null) {
throw new IOException("Resource not found: " + resourcePath);
}
Path targetPath = tempDirectory.resolve(photoName);
Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
}
}
/**
* Returns the temp directory containing test photos.
*/
public File getTestPhotosFolder() {
if (tempDirectory == null) {
throw new IllegalStateException("Call createTempDirectory() first");
}
return tempDirectory.toFile();
}
/**
* Returns a specific test photo file.
*/
public File getTestPhoto(int index) {
if (index < 0 || index >= TEST_PHOTOS.length) {
throw new IllegalArgumentException("Invalid photo index: " + index);
}
return tempDirectory.resolve(TEST_PHOTOS[index]).toFile();
}
/**
* Returns the number of test photos available.
*/
public int getTestPhotoCount() {
return TEST_PHOTOS.length;
}
/**
* Deletes the temp directory and all contents.
*/
public void cleanup() {
if (tempDirectory == null) {
return;
}
try {
Files.walk(tempDirectory)
.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
// Best effort cleanup
}
});
} catch (IOException e) {
// Best effort cleanup
}
tempDirectory = null;
}
}
Step 3: Verify compilation
Run: ./gradlew :Program:compileTestJava
Expected: BUILD SUCCESSFUL
Step 4: Commit
git add Program/src/test/java/org/jphototagger/e2e/base/TestDataManager.java
git commit -m "test: add TestDataManager for E2E test data"
Files:
Program/src/test/java/org/jphototagger/e2e/base/E2ETestBase.javaStep 1: Create E2ETestBase
Create file Program/src/test/java/org/jphototagger/e2e/base/E2ETestBase.java:
package org.jphototagger.e2e.base;
import java.awt.Frame;
import org.assertj.swing.core.BasicRobot;
import org.assertj.swing.core.Robot;
import org.assertj.swing.edt.GuiActionRunner;
import org.assertj.swing.fixture.FrameFixture;
import org.jphototagger.program.app.AppInit;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
/**
* Base class for all E2E GUI tests.
* Handles app launch, robot creation, and test data management.
*/
@Tag("e2e")
public abstract class E2ETestBase {
protected static FrameFixture window;
protected static Robot robot;
protected TestDataManager testData;
@BeforeAll
static void launchApp() {
robot = BasicRobot.robotWithNewAwtHierarchy();
// Launch JPhotoTagger on the EDT
Frame frame = GuiActionRunner.execute(() -> {
AppInit.INSTANCE.init(new String[]{});
return findMainFrame();
});
window = new FrameFixture(robot, frame);
window.show();
}
private static Frame findMainFrame() {
// Wait for main frame to be visible
Frame[] frames = Frame.getFrames();
for (Frame frame : frames) {
if (frame.isVisible() && frame.getTitle().contains("JPhotoTagger")) {
return frame;
}
}
// If not found by title, return the first visible frame
for (Frame frame : frames) {
if (frame.isVisible()) {
return frame;
}
}
throw new IllegalStateException("Main frame not found");
}
@BeforeEach
void setupTestData() throws Exception {
testData = new TestDataManager();
testData.createTempDirectory();
testData.copyTestPhotos();
}
@AfterEach
void cleanupTestData() {
if (testData != null) {
testData.cleanup();
}
}
@AfterAll
static void tearDown() {
if (window != null) {
window.cleanUp();
}
}
}
Step 2: Verify compilation
Run: ./gradlew :Program:compileTestJava
Expected: BUILD SUCCESSFUL
Step 3: Commit
git add Program/src/test/java/org/jphototagger/e2e/base/E2ETestBase.java
git commit -m "test: add E2ETestBase for GUI test infrastructure"
Files:
Modules/ImportFiles/src/org/jphototagger/importfiles/ImportImageFilesDialog.javaStep 1: Read the file to find component declarations
The dialog needs setName() calls for key components. Based on the design, add names for:
Step 2: Add setName() calls in postInitComponents()
Edit Modules/ImportFiles/src/org/jphototagger/importfiles/ImportImageFilesDialog.java.
Find the postInitComponents() method (around line 95) and add after line 108 (after lookupSkipDuplicates();):
setComponentNames();
Then add a new method after postInitComponents():
private void setComponentNames() {
setName("dialog.import");
buttonOk.setName("dialog.import.btnStart");
buttonCancel.setName("dialog.import.btnCancel");
buttonChooseSourceDir.setName("dialog.import.btnBrowseSource");
buttonChooseTargetDir.setName("dialog.import.btnBrowseTarget");
labelSourceDir.setName("dialog.import.lblSourceDir");
labelTargetDir.setName("dialog.import.lblTargetDir");
}
Step 3: Verify compilation
Run: ./gradlew :Modules:ImportFiles:compileJava
Expected: BUILD SUCCESSFUL
Step 4: Commit
git add Modules/ImportFiles/src/org/jphototagger/importfiles/ImportImageFilesDialog.java
git commit -m "feat: add component names to ImportImageFilesDialog for E2E testing"
Files:
Program/src/org/jphototagger/program/app/ui/AppFrame.javaThe menu items are added dynamically via lookup, so we need to find where the import menu item is created and named. Looking at ImportImageFilesAction.java, the menu item is created without a name.
Step 1: Modify ImportImageFilesAction to set component name
Edit Modules/ImportFiles/src/org/jphototagger/importfiles/ImportImageFilesAction.java.
Change the getMenuItem() method (around line 32):
@Override
public JMenuItem getMenuItem() {
JMenuItem item = UiFactory.menuItem(this);
item.setName("menu.file.itemImport");
MenuUtil.setMnemonics(item);
return item;
}
Step 2: Verify compilation
Run: ./gradlew :Modules:ImportFiles:compileJava
Expected: BUILD SUCCESSFUL
Step 3: Commit
git add Modules/ImportFiles/src/org/jphototagger/importfiles/ImportImageFilesAction.java
git commit -m "feat: add component name to import menu item for E2E testing"
Files:
Program/src/test/java/org/jphototagger/e2e/pages/MainWindowPage.javaStep 1: Create directory structure
Run: mkdir -p Program/src/test/java/org/jphototagger/e2e/pages
Step 2: Create MainWindowPage
Create file Program/src/test/java/org/jphototagger/e2e/pages/MainWindowPage.java:
package org.jphototagger.e2e.pages;
import org.assertj.swing.core.GenericTypeMatcher;
import org.assertj.swing.fixture.DialogFixture;
import org.assertj.swing.fixture.FrameFixture;
import org.assertj.swing.fixture.JPanelFixture;
import org.assertj.swing.fixture.JTreeFixture;
import javax.swing.JDialog;
/**
* Page object for the main JPhotoTagger window.
* Provides access to main UI components and navigation to dialogs.
*/
public class MainWindowPage {
private final FrameFixture window;
public MainWindowPage(FrameFixture window) {
this.window = window;
}
/**
* Opens the Import dialog via File menu.
*/
public ImportDialogPage openImportDialog() {
window.menuItem("menu.file.itemImport").click();
// Wait for and find the import dialog
DialogFixture dialog = window.dialog(new GenericTypeMatcher<JDialog>(JDialog.class) {
@Override
protected boolean isMatching(JDialog d) {
return "dialog.import".equals(d.getName()) ||
(d.isVisible() && d.getTitle() != null &&
d.getTitle().toLowerCase().contains("import"));
}
});
return new ImportDialogPage(dialog);
}
/**
* Returns the directory tree component.
*/
public JTreeFixture directoryTree() {
return window.tree("treeDirectories");
}
/**
* Returns the thumbnails panel.
*/
public JPanelFixture thumbnailsPanel() {
return window.panel("panelThumbnailsMetadata");
}
/**
* Returns the search panel.
*/
public JPanelFixture searchPanel() {
return window.panel("panelSearch");
}
}
Step 3: Verify compilation
Run: ./gradlew :Program:compileTestJava
Expected: Compilation error (ImportDialogPage not yet created). This is expected.
Step 4: Commit (with note about incomplete state)
git add Program/src/test/java/org/jphototagger/e2e/pages/MainWindowPage.java
git commit -m "test: add MainWindowPage (partial, requires ImportDialogPage)"
Files:
Program/src/test/java/org/jphototagger/e2e/pages/ImportDialogPage.javaStep 1: Create ImportDialogPage
Create file Program/src/test/java/org/jphototagger/e2e/pages/ImportDialogPage.java:
package org.jphototagger.e2e.pages;
import org.assertj.swing.fixture.DialogFixture;
import java.io.File;
/**
* Page object for the Import Images dialog.
*/
public class ImportDialogPage {
private final DialogFixture dialog;
public ImportDialogPage(DialogFixture dialog) {
this.dialog = dialog;
}
/**
* Sets the source directory by clicking browse and selecting folder.
* Note: This is a simplified version that sets the label directly.
* For full testing, would need to handle the directory chooser.
*/
public ImportDialogPage withSourceDirectory(File folder) {
// Click browse button to open directory chooser
dialog.button("dialog.import.btnBrowseSource").click();
// For now, we'll need to handle the native file chooser
// This may require robot.keyPress to type the path
// TODO: Implement directory chooser handling
return this;
}
/**
* Clicks the Import (OK) button.
*/
public void clickImport() {
dialog.button("dialog.import.btnStart").click();
}
/**
* Clicks the Cancel button.
*/
public void clickCancel() {
dialog.button("dialog.import.btnCancel").click();
}
/**
* Verifies the dialog is visible.
*/
public ImportDialogPage requireVisible() {
dialog.requireVisible();
return this;
}
/**
* Verifies the dialog is not visible (closed).
*/
public void requireNotVisible() {
dialog.requireNotVisible();
}
/**
* Returns the underlying dialog fixture for advanced assertions.
*/
public DialogFixture dialog() {
return dialog;
}
}
Step 2: Verify compilation
Run: ./gradlew :Program:compileTestJava
Expected: BUILD SUCCESSFUL
Step 3: Commit
git add Program/src/test/java/org/jphototagger/e2e/pages/ImportDialogPage.java
git commit -m "test: add ImportDialogPage for import workflow testing"
Files:
Program/src/test/java/org/jphototagger/e2e/workflows/ImportWorkflowTest.javaStep 1: Create directory structure
Run: mkdir -p Program/src/test/java/org/jphototagger/e2e/workflows
Step 2: Create ImportWorkflowTest
Create file Program/src/test/java/org/jphototagger/e2e/workflows/ImportWorkflowTest.java:
package org.jphototagger.e2e.workflows;
import org.jphototagger.e2e.base.E2ETestBase;
import org.jphototagger.e2e.pages.ImportDialogPage;
import org.jphototagger.e2e.pages.MainWindowPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* E2E tests for the photo import workflow.
*/
class ImportWorkflowTest extends E2ETestBase {
private MainWindowPage mainWindow;
@BeforeEach
void setup() {
mainWindow = new MainWindowPage(window);
}
@Test
@DisplayName("Import dialog opens from File menu")
void openImportDialog_displaysImportDialog() {
ImportDialogPage importDialog = mainWindow.openImportDialog();
importDialog.requireVisible();
// Close dialog to clean up
importDialog.clickCancel();
}
@Test
@DisplayName("Import dialog can be cancelled")
void cancelImportDialog_closesDialog() {
ImportDialogPage importDialog = mainWindow.openImportDialog();
importDialog.clickCancel();
importDialog.requireNotVisible();
}
@Test
@Disabled("Requires directory chooser handling - implement in next iteration")
@DisplayName("Import photos from folder displays in thumbnail panel")
void importPhotosFromFolder_displaysInThumbnailPanel() {
// This test requires handling the native directory chooser
// which is complex with AssertJ Swing.
// Keeping as placeholder for next iteration.
ImportDialogPage importDialog = mainWindow.openImportDialog();
importDialog.withSourceDirectory(testData.getTestPhotosFolder());
importDialog.clickImport();
// Verify photos appear in thumbnail panel
assertThat(testData.getTestPhotoCount()).isEqualTo(3);
}
}
Step 3: Verify compilation
Run: ./gradlew :Program:compileTestJava
Expected: BUILD SUCCESSFUL
Step 4: Commit
git add Program/src/test/java/org/jphototagger/e2e/workflows/ImportWorkflowTest.java
git commit -m "test: add ImportWorkflowTest with initial dialog tests"
Files:
.github/workflows/build.ymlStep 1: Add E2E test job with Xvfb
Edit .github/workflows/build.yml, add after line 63 (after benchmark job):
e2e-tests:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Run E2E tests with Xvfb
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" ./gradlew :Program:e2eTest
- name: Upload E2E test results
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-test-results
path: 'Program/build/test-results/e2eTest/*.xml'
Step 2: Verify YAML syntax
Run: cat .github/workflows/build.yml | python3 -c "import sys, yaml; yaml.safe_load(sys.stdin); print('YAML is valid')"
Expected: "YAML is valid"
Step 3: Commit
git add .github/workflows/build.yml
git commit -m "ci: add E2E test job with Xvfb"
Step 1: Run E2E tests
Run: ./gradlew :Program:e2eTest
Expected: Tests run. May see failures if display not available.
If on Linux without display, run with Xvfb:
Run: xvfb-run --auto-servernum ./gradlew :Program:e2eTest
Step 2: Review test output
Check: Program/build/reports/tests/e2eTest/index.html
Expected: Test report shows test execution
Step 3: Document any issues found
If tests fail, note the failure reason for iteration.
Step 1: Verify all files created
Run: find Program/src/test/java/org/jphototagger/e2e -name "*.java" | wc -l
Expected: 4 files (E2ETestBase, TestDataManager, MainWindowPage, ImportDialogPage, ImportWorkflowTest... actually 5 files, so expect 5)
Actually:
base/E2ETestBase.javabase/TestDataManager.javapages/MainWindowPage.javapages/ImportDialogPage.javaworkflows/ImportWorkflowTest.javaRun: find Program/src/test/java/org/jphototagger/e2e -name "*.java"
Expected: 5 Java files
Step 2: Run full build
Run: ./gradlew build
Expected: BUILD SUCCESSFUL
Step 3: Create summary commit
git add -A
git status
If any unstaged changes, commit:
git commit -m "test: complete Phase 1 E2E testing infrastructure
- AssertJ Swing dependency added
- TestDataManager for test photo lifecycle
- E2ETestBase for shared test setup
- MainWindowPage and ImportDialogPage (page objects)
- ImportWorkflowTest with initial dialog tests
- GitHub Actions e2e-tests job with Xvfb
Implements Phase 1 of GUI automation design."
After Phase 1 is complete and tests pass:
Phase 2: Keyword Tagging Workflow
setName() calls to KeywordsPanel componentsKeywordPanelPage.javaKeywordTaggingWorkflowTest.javaPhase 3: Search Workflow
setName() calls to search panel componentsSearchPanelPage.javaSearchWorkflowTest.javaPhase 4: Enhanced Import Testing