mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-16 23:35:46 +00:00
Compare commits
2 Commits
craft_chan
...
craft-late
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ebe9e221 | ||
|
|
99c9c378cd |
8
.github/workflows/pr-playwright-tests.yml
vendored
8
.github/workflows/pr-playwright-tests.yml
vendored
@@ -249,7 +249,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
project: [admin, no-auth, exclusive]
|
||||
project: [admin, exclusive]
|
||||
steps:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
@@ -299,9 +299,6 @@ jobs:
|
||||
ONYX_MODEL_SERVER_IMAGE=${ECR_CACHE}:playwright-test-model-server-${RUN_ID}
|
||||
ONYX_WEB_SERVER_IMAGE=${ECR_CACHE}:playwright-test-web-${RUN_ID}
|
||||
EOF
|
||||
if [ "${{ matrix.project }}" = "no-auth" ]; then
|
||||
echo "PLAYWRIGHT_FORCE_EMPTY_LLM_PROVIDERS=true" >> deployment/docker_compose/.env
|
||||
fi
|
||||
|
||||
# needed for pulling Vespa, Redis, Postgres, and Minio images
|
||||
# otherwise, we hit the "Unauthenticated users" limit
|
||||
@@ -430,9 +427,6 @@ jobs:
|
||||
run: |
|
||||
# Create test-results directory to ensure it exists for artifact upload
|
||||
mkdir -p test-results
|
||||
if [ "${PROJECT}" = "no-auth" ]; then
|
||||
export PLAYWRIGHT_FORCE_EMPTY_LLM_PROVIDERS=true
|
||||
fi
|
||||
npx playwright test --project ${PROJECT}
|
||||
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
|
||||
@@ -149,6 +149,11 @@ RUN if [ "$ENABLE_CRAFT" = "true" ]; then \
|
||||
ENABLE_CRAFT=true /app/scripts/setup_craft_templates.sh; \
|
||||
fi
|
||||
|
||||
# Set Craft template paths to the in-image locations
|
||||
# These match the paths where setup_craft_templates.sh creates the templates
|
||||
ENV OUTPUTS_TEMPLATE_PATH=/app/onyx/server/features/build/sandbox/kubernetes/docker/templates/outputs
|
||||
ENV VENV_TEMPLATE_PATH=/app/onyx/server/features/build/sandbox/kubernetes/docker/templates/venv
|
||||
|
||||
# Put logo in assets
|
||||
COPY --chown=onyx:onyx ./assets /app/assets
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ with SKIP_AUTH=true. This is convenient but slightly different from what happens
|
||||
in CI so tests might pass locally and fail in CI.
|
||||
|
||||
```cd web
|
||||
SKIP_AUTH=true npx playwright test create_and_edit_assistant.spec.ts --project=no-auth
|
||||
SKIP_AUTH=true npx playwright test create_and_edit_assistant.spec.ts --project=admin
|
||||
```
|
||||
|
||||
2. Run playwright
|
||||
|
||||
45
web/package-lock.json
generated
45
web/package-lock.json
generated
@@ -71,7 +71,6 @@
|
||||
"react-loader-spinner": "^8.0.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-truncate-markup": "^5.1.2",
|
||||
"recharts": "^2.13.1",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@@ -8561,11 +8560,6 @@
|
||||
"version": "1.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/computed-style": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz",
|
||||
"integrity": "sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"dev": true,
|
||||
@@ -12854,18 +12848,6 @@
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/line-height": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz",
|
||||
"integrity": "sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"computed-style": "~0.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"license": "MIT"
|
||||
@@ -15907,27 +15889,6 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-truncate-markup": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-truncate-markup/-/react-truncate-markup-5.1.2.tgz",
|
||||
"integrity": "sha512-eEq6T8Rs+wz98cRYzQECGFNBfXwRYraLg/kz52f6DRBKmzxqB+GYLeDkVe/zrC+2vh5AEwM6nSYFvDWEBljd0w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"line-height": "0.3.1",
|
||||
"memoize-one": "^5.1.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"resize-observer-polyfill": "1.5.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-truncate-markup/node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
@@ -16234,12 +16195,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-cwd": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"react-loader-spinner": "^8.0.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-truncate-markup": "^5.1.2",
|
||||
"recharts": "^2.13.1",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
|
||||
@@ -48,14 +48,6 @@ export default defineConfig({
|
||||
},
|
||||
grepInvert: /@exclusive/,
|
||||
},
|
||||
{
|
||||
name: "no-auth",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
grepInvert: /@exclusive/,
|
||||
},
|
||||
{
|
||||
// this suite runs independently and serially + slower
|
||||
// we should be cautious about bloating this suite
|
||||
|
||||
@@ -2,10 +2,6 @@ import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"
|
||||
import { fetchSS } from "../utilsSS";
|
||||
|
||||
export async function fetchLLMProvidersSS() {
|
||||
// Test helper: allow Playwright runs to force an empty provider list so onboarding appears.
|
||||
if (process.env.PLAYWRIGHT_FORCE_EMPTY_LLM_PROVIDERS === "true") {
|
||||
return [];
|
||||
}
|
||||
const response = await fetchSS("/llm/provider");
|
||||
if (response.ok) {
|
||||
return (await response.json()) as LLMProviderDescriptor[];
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useMemo, useRef, useLayoutEffect, useEffect } from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import TruncateMarkup from "react-truncate-markup";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
@@ -101,8 +100,7 @@ export default function ExpandableTextDisplay({
|
||||
const contentSize = useMemo(() => getContentSize(content), [content]);
|
||||
const displaySubtitle = subtitle ?? contentSize;
|
||||
|
||||
// Detect truncation for renderContent mode and streaming
|
||||
// (TruncateMarkup's onTruncate handles plain text static mode)
|
||||
// Detect truncation for renderContent mode, streaming, and plain text static
|
||||
useLayoutEffect(() => {
|
||||
if (renderContent && scrollRef.current) {
|
||||
// For renderContent mode (streaming or static), use scroll-based detection
|
||||
@@ -115,8 +113,12 @@ export default function ExpandableTextDisplay({
|
||||
const textToCheck = displayContent ?? content;
|
||||
const lineCount = getLineCount(textToCheck);
|
||||
setIsTruncated(lineCount > maxLines);
|
||||
} else if (scrollRef.current) {
|
||||
// For plain text static, use scroll-based detection with line-clamp
|
||||
setIsTruncated(
|
||||
scrollRef.current.scrollHeight > scrollRef.current.clientHeight
|
||||
);
|
||||
}
|
||||
// Plain text static mode is handled by TruncateMarkup's onTruncate
|
||||
}, [isStreaming, renderContent, content, displayContent, maxLines]);
|
||||
|
||||
// Scroll to bottom during streaming for renderContent mode
|
||||
@@ -132,11 +134,6 @@ export default function ExpandableTextDisplay({
|
||||
prevIsStreamingRef.current = isStreaming;
|
||||
}, [isStreaming]);
|
||||
|
||||
// Handle truncation callback from TruncateMarkup (static mode only)
|
||||
const handleTruncate = (wasTruncated: boolean) => {
|
||||
setIsTruncated(wasTruncated);
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const sanitizedTitle = title.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
||||
downloadAsTxt(content, sanitizedTitle);
|
||||
@@ -214,15 +211,13 @@ export default function ExpandableTextDisplay({
|
||||
);
|
||||
};
|
||||
|
||||
// Render plain text static (TruncateMarkup for reliable truncation)
|
||||
// Render plain text static (CSS line-clamp + scroll-based truncation detection)
|
||||
const renderPlainTextStatic = () => (
|
||||
<TruncateMarkup lines={maxLines} ellipsis="…" onTruncate={handleTruncate}>
|
||||
<div className="whitespace-pre-wrap">
|
||||
<Text as="p" mainUiMuted text03>
|
||||
{displayContent ?? content}
|
||||
</Text>
|
||||
</div>
|
||||
</TruncateMarkup>
|
||||
<div ref={scrollRef} className={cn("overflow-hidden", lineClampClass)}>
|
||||
<Text as="span" mainUiMuted text03 className="whitespace-pre-wrap">
|
||||
{displayContent ?? content}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
import { test, expect } from "@chromatic-com/playwright";
|
||||
import { Route } from "@playwright/test";
|
||||
import { loginAs } from "@tests/e2e/utils/auth";
|
||||
|
||||
test.describe("First user onboarding flow", () => {
|
||||
test("completes onboarding wizard and unlocks chat input", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name !== "no-auth",
|
||||
"Onboarding flow requires a clean session without preset auth state"
|
||||
);
|
||||
|
||||
// Track whether we've "created" a provider for this run.
|
||||
let providerCreated = false;
|
||||
|
||||
// Force an empty provider list at first so onboarding shows, then return
|
||||
// a stub provider after the Connect flow completes.
|
||||
const providerListResponder = async (route: Route) => {
|
||||
if (route.request().method() !== "GET") {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
const body = providerCreated
|
||||
? JSON.stringify([
|
||||
{
|
||||
id: 1,
|
||||
name: "OpenAI",
|
||||
provider: "openai",
|
||||
is_default_provider: true,
|
||||
default_model_name: "gpt-4o",
|
||||
model_configurations: [{ name: "gpt-4o", is_visible: true }],
|
||||
},
|
||||
])
|
||||
: "[]";
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
await page.route("**/api/llm/provider", providerListResponder);
|
||||
await page.route("**/llm/provider", providerListResponder);
|
||||
|
||||
// Mock provider creation/update endpoints so fake keys still succeed.
|
||||
await page.route(
|
||||
"**/api/admin/llm/provider?is_creation=true",
|
||||
async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
providerCreated = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id: 1,
|
||||
name: "OpenAI",
|
||||
provider: "openai",
|
||||
is_default_provider: true,
|
||||
default_model_name: "gpt-4o",
|
||||
model_configurations: [{ name: "gpt-4o", is_visible: true }],
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.continue();
|
||||
}
|
||||
);
|
||||
|
||||
await page.route("**/api/admin/llm/provider/*/default", async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: "{}",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await page.route(
|
||||
(url) => url.pathname.endsWith("/api/admin/llm/test"),
|
||||
async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
}
|
||||
);
|
||||
await page.context().clearCookies();
|
||||
await loginAs(page, "admin");
|
||||
|
||||
// Reset the admin user's personalization to ensure onboarding starts from step 1
|
||||
await page.evaluate(async () => {
|
||||
await fetch("/api/user/personalization", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ name: "" }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/app");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const dismissNewTeamModal = async () => {
|
||||
const continueButton = page
|
||||
.getByRole("button", { name: /Continue with new team/i })
|
||||
.first();
|
||||
if ((await continueButton.count()) > 0) {
|
||||
await continueButton.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
const tryOnyxButton = page
|
||||
.getByRole("button", { name: /Try Onyx while waiting/i })
|
||||
.first();
|
||||
if ((await tryOnyxButton.count()) > 0) {
|
||||
await tryOnyxButton.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const dismissed = await dismissNewTeamModal();
|
||||
if (dismissed) {
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
const onboardingTitle = page
|
||||
.getByText("Let's take a moment to get you set up.")
|
||||
.first();
|
||||
await expect(onboardingTitle).toBeVisible({ timeout: 20000 });
|
||||
|
||||
const letsGoButton = page.getByRole("button", { name: "Let's Go" });
|
||||
await expect(letsGoButton).toBeEnabled();
|
||||
await letsGoButton.click();
|
||||
|
||||
await expect(page.getByText("Step 1 of 3")).toBeVisible();
|
||||
await expect(page.getByText("What should Onyx call you?")).toBeVisible();
|
||||
|
||||
const nameInput = page.getByPlaceholder("Your name").first();
|
||||
await nameInput.fill("Playwright Tester");
|
||||
await expect(nameInput).toHaveValue("Playwright Tester");
|
||||
|
||||
const nextButton = page.getByRole("button", { name: "Next", exact: true });
|
||||
await expect(nextButton).toBeEnabled();
|
||||
await nextButton.click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByText("Almost there! Connect your models to start chatting.")
|
||||
.first()
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Step 2 of 3")).toBeVisible();
|
||||
|
||||
const providerCards = [
|
||||
{ title: "GPT", subtitle: "OpenAI" },
|
||||
{ title: "Claude", subtitle: "Anthropic" },
|
||||
{ title: "Azure OpenAI", subtitle: "Microsoft Azure Cloud" },
|
||||
{ title: "Amazon Bedrock", subtitle: "AWS" },
|
||||
{ title: "Gemini", subtitle: "Google Cloud Vertex AI" },
|
||||
{ title: "OpenRouter", subtitle: "OpenRouter" },
|
||||
{ title: "Ollama", subtitle: "Ollama" },
|
||||
{ title: "Custom LLM Provider", subtitle: "LiteLLM Compatible APIs" },
|
||||
];
|
||||
|
||||
for (const provider of providerCards) {
|
||||
await expect(
|
||||
page
|
||||
.getByRole("button", {
|
||||
name: new RegExp(`${provider.title}.*${provider.subtitle}`, "i"),
|
||||
})
|
||||
.first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
const openaiCard = page
|
||||
.getByRole("button", { name: /GPT.*OpenAI/i })
|
||||
.first();
|
||||
await openaiCard.click();
|
||||
|
||||
const providerModal = page.getByRole("dialog", { name: /Set up GPT/i });
|
||||
await expect(providerModal).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const apiKeyInput = page
|
||||
.getByLabel("API Key", { exact: false })
|
||||
.or(page.locator('input[type="password"]').first());
|
||||
await apiKeyInput.fill("sk-onboarding-test-key");
|
||||
|
||||
await page.getByRole("button", { name: "Connect" }).click();
|
||||
|
||||
await expect(providerModal).toBeHidden({ timeout: 15000 });
|
||||
|
||||
await expect(nextButton).toBeEnabled({ timeout: 10000 });
|
||||
await nextButton.click();
|
||||
|
||||
const completionHeading = page
|
||||
.getByText(
|
||||
"You're all set, review the optional settings or click Finish Setup"
|
||||
)
|
||||
.first();
|
||||
await expect(completionHeading).toBeVisible();
|
||||
await expect(page.getByText("Step 3 of 3")).toBeVisible();
|
||||
|
||||
const checklistItems = [
|
||||
"Select web search provider",
|
||||
"Enable image generation",
|
||||
"Invite your team",
|
||||
];
|
||||
for (const item of checklistItems) {
|
||||
await expect(page.getByText(item).first()).toBeVisible();
|
||||
}
|
||||
|
||||
const finishSetupButton = page.getByRole("button", {
|
||||
name: "Finish Setup",
|
||||
});
|
||||
await finishSetupButton.click();
|
||||
await expect(finishSetupButton).toBeHidden({ timeout: 5000 });
|
||||
|
||||
await expect(page.getByText("Connect your LLM models")).toHaveCount(0);
|
||||
|
||||
const chatInput = page.locator("#onyx-chat-input-textarea");
|
||||
await chatInput.waitFor({ state: "visible", timeout: 10000 });
|
||||
await chatInput.fill("Hello from onboarding");
|
||||
await expect(chatInput).toHaveValue("Hello from onboarding");
|
||||
await chatInput.fill("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user