Skip to content

Fix delete, double slashes, e2e tests #1076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions .dagger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class AtomicServer {
"**/.envrc",
],
})
source: Directory
source: Directory,
) {
this.source = source;
}
Expand Down Expand Up @@ -107,7 +107,7 @@ export class AtomicServer {
/** The directory to deploy */
directory: Directory,
siteName: string,
netlifyAuthToken: Secret
netlifyAuthToken: Secret,
): Promise<string> {
return dag
.container()
Expand Down Expand Up @@ -135,10 +135,13 @@ export class AtomicServer {
docsFolder(): Directory {
const cargoCache = dag.cacheVolume("cargo");

const cargoHomeCache = dag.cacheVolume("cargo_home");

const mdBookContainer = dag
.container()
.from(RUST_IMAGE)
.withMountedCache("/usr/local/cargo/registry", cargoCache)
.withMountedCache("/root/.cargo", cargoHomeCache) // Cache the Cargo home directory
.withExec(["cargo", "install", "mdbook"])
.withExec(["cargo", "install", "mdbook-linkcheck"]);

Expand Down Expand Up @@ -179,7 +182,7 @@ export class AtomicServer {
.withFile("/app/pnpm-workspace.yaml", source.file("pnpm-workspace.yaml"))
.withFile(
"/app/data-browser/package.json",
source.file("data-browser/package.json")
source.file("data-browser/package.json"),
)
.withFile("/app/lib/package.json", source.file("lib/package.json"))
.withFile("/app/react/package.json", source.file("react/package.json"))
Expand All @@ -204,7 +207,7 @@ export class AtomicServer {
/** Builds the Rust server binary on the host architecture */
rustBuild(
@argument() release: boolean = false,
@argument() target: string = "x86_64-unknown-linux-musl"
@argument() target: string = "x86_64-unknown-linux-musl",
): Container {
const source = this.source;
const cargoCache = dag.cacheVolume("cargo");
Expand Down Expand Up @@ -235,7 +238,7 @@ export class AtomicServer {
const browserDir = this.jsBuild().directory("/app/data-browser/dist");
const containerWithAssets = sourceContainer.withDirectory(
"/code/server/assets_tmp",
browserDir
browserDir,
);

const buildArgs = release
Expand All @@ -256,7 +259,7 @@ export class AtomicServer {
@func()
/** Returns the release binary */
rustBuildRelease(
@argument() target: string = "x86_64-unknown-linux-musl"
@argument() target: string = "x86_64-unknown-linux-musl",
): File {
const container = this.rustBuild(true, target);
return container.file("/atomic-server-binary");
Expand Down Expand Up @@ -390,7 +393,7 @@ export class AtomicServer {
])
.withEnvVariable(
"PATH",
"/root/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
"/root/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
)
.withExec(["pnpm", "dlx", "playwright", "install", "--with-deps"])
.withExec(["npm", "install", "-g", "netlify-cli"]);
Expand All @@ -403,6 +406,7 @@ export class AtomicServer {
.withEnvVariable("LANGUAGE", "en_GB")
.withEnvVariable("DELETE_PREVIOUS_TEST_DRIVES", "false")
.withEnvVariable("FRONTEND_URL", "http://atomic:9883")
.withEnvVariable("SERVER_URL", "http://atomic:9883")
.withServiceBinding("atomic", this.atomicService())
// Wait for the server to be ready
.withExec([
Expand All @@ -422,7 +426,7 @@ export class AtomicServer {
const deployOutput = await this.netlifyDeploy(
testReportDirectory,
"atomic-tests",
netlifyAuthToken
netlifyAuthToken,
);

// Extract the deploy URL
Expand All @@ -432,7 +436,7 @@ export class AtomicServer {
const exitCode = await e2eContainer.file("/test-exit-code").contents();
if (exitCode.trim() !== "0") {
throw new Error(
`E2E tests failed (exit code: ${exitCode.trim()}). Test report deployed to: \n${deployUrl}`
`E2E tests failed (exit code: ${exitCode.trim()}). Test report deployed to: \n${deployUrl}`,
);
}

Expand All @@ -443,7 +447,7 @@ export class AtomicServer {
async deployServer(
@argument() remoteHost: string,
@argument() remoteUser: Secret,
@argument() sshPrivateKey: Secret
@argument() sshPrivateKey: Secret,
): Promise<string> {
// Build the cross-compiled binary for x86_64-unknown-linux-musl
const binaryFile = this.rustBuildRelease("x86_64-unknown-linux-musl");
Expand Down Expand Up @@ -516,7 +520,7 @@ export class AtomicServer {
for (const build of builds) {
outputDir = outputDir.withFile(
`atomic-server-${build.target}`,
build.binary
build.binary,
);
}

Expand All @@ -526,7 +530,7 @@ export class AtomicServer {
@func()
/** Creates a Docker image for a specific target architecture */
createDockerImage(
@argument() target: string = "x86_64-unknown-linux-musl"
@argument() target: string = "x86_64-unknown-linux-musl",
): Container {
const binary = this.rustBuild(true, target).file("/atomic-server-binary");

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain

- [#1048](https://github.com/atomicdata-dev/atomic-server/issues/1048) Fix search index not removing old versions of resources.
- [#1056](https://github.com/atomicdata-dev/atomic-server/issues/1056) Switched from Earthly to Dagger for CI. Also made improvements to E2E test publishing and building docker images.
- [#979](https://github.com/atomicdata-dev/atomic-server/issues/979) Fix nested resource deletion, use transactions
- [#1057](https://github.com/atomicdata-dev/atomic-server/issues/1057) Fix double slashes in search bar

## [v0.40.2]

Expand Down
2 changes: 0 additions & 2 deletions browser/data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,8 @@
},
"scripts": {
"build": "vite build",
"deploy": "gh-pages -d build",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"lint-fix": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix",
"predeploy": "build && touch build/.nojekyll",
"preview": "vite preview",
"start": "vite",
"test": "vitest run",
Expand Down
3 changes: 2 additions & 1 deletion browser/data-browser/src/helpers/useCurrentSubject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function useCurrentSubject(
replace?: boolean,
): [string | undefined, setFunc] {
const { subject: subjectQ } = useSearch({ strict: false });

const navigate = useNavigateWithTransition();
const navigateShow = ShowRoute.useNavigate();
const { pathname } = useLocation();
Expand All @@ -37,7 +38,7 @@ export function useCurrentSubject(
}

// The pathname defaults to a trailing slash, which leads to issues
const correctedPathName = pathname === '/' ? '' : '/' + pathname;
const correctedPathName = pathname === '/' ? '' : pathname;
const subject =
window.location.origin + correctedPathName + window.location.search;

Expand Down
5 changes: 4 additions & 1 deletion browser/data-browser/src/routes/RootRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ const TopRouteComponent: React.FC = () => {
// We need to combine origin with tanstack's href because tanstack does not include the origin in the href but the normal window.location.href is not reactive.
const subject = window.location.origin + href;

return <ResourcePage subject={subject} key={subject} />;
// Remove trailing slash from subject
const cleanedSubject = subject.endsWith('/') ? subject.slice(0, -1) : subject;

return <ResourcePage subject={cleanedSubject} key={cleanedSubject} />;
};

export const topRoute = createRoute({
Expand Down
59 changes: 42 additions & 17 deletions browser/e2e/tests/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
openSubject,
publicReadRightLocator,
setTitle,
sideBarDriveSwitcher,
signIn,
timestamp,
waitForCommit,
Expand Down Expand Up @@ -311,22 +310,22 @@ test.describe('data-browser', async () => {
).toBeVisible();
});

test('drive switcher', async ({ page }) => {
await signIn(page);
await page.click(sideBarDriveSwitcher);
// temp disable for trailing slash
// const dropdownId = await page
// .locator(sideBarDriveSwitcher)
// .getAttribute('aria-controls');
// await page.click(`[id="${dropdownId}"] >> text=Atomic Data`);
// await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data');

// Cleanup drives for signed in user
await openAgentPage(page);
await page.click('text=Edit profile');
await page.getByTestId('input-drives-clear').click();
await page.click('[data-test="save"]');
});
// test('drive switcher', async ({ page }) => {
// await signIn(page);
// await page.click(sideBarDriveSwitcher);
// // temp disable for trailing slash
// // const dropdownId = await page
// // .locator(sideBarDriveSwitcher)
// // .getAttribute('aria-controls');
// // await page.click(`[id="${dropdownId}"] >> text=Atomic Data`);
// // await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data');

// // Cleanup drives for signed in user
// await openAgentPage(page);
// await page.click('text=Edit profile');
// await page.getByTestId('input-drives-clear').click();
// await page.click('[data-test="save"]');
// });

test('configure drive page', async ({ page }) => {
await signIn(page);
Expand Down Expand Up @@ -386,6 +385,32 @@ test.describe('data-browser', async () => {
await expect(page.locator('text=Resource Saved')).toBeVisible();
});

test('delete resource', async ({ page }) => {
await signIn(page);
await newDrive(page);
await newResource('folder', page);
// Create a nested resource
const parentResource = await getCurrentSubject(page);
await page.click('button:has-text("New Resource")');
await page.click('button:has-text("folder")');
// Get current URL
const nestedResource = await getCurrentSubject(page);
await openSubject(page, parentResource);
await contextMenuClick('delete', page);
await page.click('button:has-text("Delete")');

await expect(page.locator('text=Resource deleted')).toBeVisible();

await page.reload();
await openSubject(page, nestedResource);

// Expect a 404
await expect(
page.locator('text=Resource not found'),
'Nested resource not deleted',
).toBeVisible();
});

test('sidebar subresource', async ({ page }) => {
await signIn(page);
await newDrive(page);
Expand Down
5 changes: 5 additions & 0 deletions browser/e2e/tests/search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,17 @@ test.describe('search', async () => {

// Search for the folder by the first tag
await addressBar(page).fill('tag:first');
await expect(page.locator(`text=${firstTagName}`).first()).toBeVisible();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');

// Verify the folder is found in search results
await expect(page.getByRole('heading', { name: folderName })).toBeVisible();

// Search for the folder by the second tag
await addressBar(page).fill(`tag:${secondTagName}`);
await expect(page.locator(`text=${secondTagName}`).first()).toBeVisible();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');

// Verify the folder is found in search results
Expand All @@ -130,6 +134,7 @@ test.describe('search', async () => {
// Verify that searching for a non-existent tag doesn't find the folder
const nonExistentTag = `nonexistent-tag`;
await addressBar(page).fill(`tag:${nonExistentTag}`);
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');

// Verify the folder is not found
Expand Down
16 changes: 12 additions & 4 deletions browser/e2e/tests/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Page, expect, Browser, Locator } from '@playwright/test';

export const SERVER_URL = 'http://localhost:9883';
export const DELETE_PREVIOUS_TEST_DRIVES =
process.env.DELETE_PREVIOUS_TEST_DRIVES === 'false' ? false : true;

export const SERVER_URL = process.env.SERVER_URL || 'http://localhost:9883';
export const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
// TODO: Should use an env var so the CI can test the setup test.
export const INITIAL_TEST = false;
Expand Down Expand Up @@ -105,7 +105,7 @@ export async function newDrive(page: Page) {
await expect(currentDriveTitle(page)).not.toHaveText('localhost');
await expect(currentDriveTitle(page)).toHaveText(driveTitle);
const driveURL = await getCurrentSubject(page);
expect(driveURL).toContain(FRONTEND_URL);
expect(driveURL).toContain(SERVER_URL);

return { driveURL: driveURL as string, driveTitle };
}
Expand All @@ -128,10 +128,16 @@ export async function openSubject(page: Page, subject: string) {
await expect(page.locator(`main[about="${subject}"]`).first()).toBeVisible();
}

export async function getCurrentSubject(page: Page) {
export async function getCurrentSubject(page: Page): Promise<string> {
const selector = await page.waitForSelector('main[about]');

return selector.getAttribute('about');
const about = await selector.getAttribute('about');

if (!about) {
throw new Error('No subject found (no `main[about]` found)');
}

return about;
}

/** Waits until a commit for main resource is processed
Expand Down Expand Up @@ -242,6 +248,8 @@ export async function newResource(klass: string, page: Page) {
await page.keyboard.press('Enter');
} else {
await page.locator(`button:has-text("${klass}")`).click();
// after navigation to the new resource, wait for the URL to change
await page.locator('main[about]');
}
}

Expand Down
8 changes: 7 additions & 1 deletion browser/lib/src/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export function applyCommitToResource(
resource: Resource,
commit: Commit,
): Resource {
const { set, remove, push } = commit;
const { set, remove, push, destroy } = commit;

if (set) {
execSetCommit(set, resource);
Expand All @@ -459,6 +459,12 @@ export function applyCommitToResource(
execPushCommit(push, resource);
}

if (destroy) {
for (const [key] of resource.getPropVals()) {
resource.setUnsafe(key, undefined);
}
}

return resource;
}

Expand Down
Loading
Loading