Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ jobs:
- name: Install
run: npm ci


- name: Test
run: npm run test

- name: Cypress run
run: npm run cy:run

Expand Down
5 changes: 3 additions & 2 deletions cypress.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
retries: {
runMode: 2,
openMode: 0,
},
},
});
90 changes: 90 additions & 0 deletions cypress/common/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ApiClient, createApiClient, components } from "@tginternal/client";
import { languagesTestData } from "./languageTestData";

const API_URL = "http://localhost:22223";

export async function deleteProject(client: ApiClient | undefined) {
await client?.DELETE("/v2/projects/{projectId}", {
params: { path: { projectId: client?.getProjectId() } },
parseAs: "stream",
});
}

export type Options = {
languages?: components["schemas"]["LanguageRequest"][];
} & Partial<Omit<components["schemas"]["EditProjectRequest"], "name">>;

export async function createProjectWithClient(
name: string,
data: components["schemas"]["SingleStepImportResolvableRequest"],
options?: Options
) {
const client = createApiClient({
baseUrl: API_URL,
});
await client.login({ username: "admin", password: "admin" });
const organizations = await client.GET("/v2/organizations");
const { languages, ...editOptions } = options ?? {};

const project = await client.POST("/v2/projects", {
body: {
name,
organizationId: organizations.data!._embedded!.organizations![0].id,
languages: languages ?? languagesTestData,
icuPlaceholders: editOptions?.icuPlaceholders ?? true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential configuration override: icuPlaceholders is set twice.

Line 34 defaults icuPlaceholders to true if not provided in editOptions, but line 47 unconditionally sets it to true again in the PUT request, potentially overriding a user-provided false value from editOptions.

Apply this diff to ensure editOptions takes precedence:

    body: {
      name,
      organizationId: organizations.data!._embedded!.organizations![0].id,
      languages: languages ?? languagesTestData,
-     icuPlaceholders: editOptions?.icuPlaceholders ?? true,
+     icuPlaceholders: true,
    },
  });

  client.setProjectId(project.data!.id);

  await client.PUT("/v2/projects/{projectId}", {
    params: {
      path: {
        projectId: client.getProjectId(),
      },
    },
    body: {
-     icuPlaceholders: true,
      useNamespaces: true,
      suggestionsMode: "DISABLED",
      translationProtection: "NONE",
      ...editOptions,
+     icuPlaceholders: editOptions?.icuPlaceholders ?? true,
      name,
    },
  });

Also applies to: 47-47

🤖 Prompt for AI Agents
In cypress/common/apiClient.ts around lines 34 and 47, icuPlaceholders is
defaulted from editOptions at line 34 but then overwritten unconditionally to
true in the PUT request at line 47; change the payload at line 47 to use the
editOptions value (e.g., icuPlaceholders: editOptions?.icuPlaceholders ?? true)
instead of hardcoding true so a user-provided false is preserved.

},
});
Comment on lines +29 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unsafe chained non-null assertions could cause runtime errors.

Line 32 chains multiple non-null assertions (organizations.data!._embedded!.organizations![0].id). If any of these properties are undefined or the organizations array is empty, this will throw a runtime error.

Apply this diff to add proper error handling:

  const organizations = await client.GET("/v2/organizations");
+ if (!organizations.data?._embedded?.organizations?.length) {
+   throw new Error("No organizations found for admin user");
+ }
  const { languages, ...editOptions } = options ?? {};

  const project = await client.POST("/v2/projects", {
    body: {
      name,
-     organizationId: organizations.data!._embedded!.organizations![0].id,
+     organizationId: organizations.data._embedded.organizations[0].id,
      languages: languages ?? languagesTestData,
      icuPlaceholders: editOptions?.icuPlaceholders ?? true,
    },
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const project = await client.POST("/v2/projects", {
body: {
name,
organizationId: organizations.data!._embedded!.organizations![0].id,
languages: languages ?? languagesTestData,
icuPlaceholders: editOptions?.icuPlaceholders ?? true,
},
});
const organizations = await client.GET("/v2/organizations");
if (!organizations.data?._embedded?.organizations?.length) {
throw new Error("No organizations found for admin user");
}
const { languages, ...editOptions } = options ?? {};
const project = await client.POST("/v2/projects", {
body: {
name,
organizationId: organizations.data._embedded.organizations[0].id,
languages: languages ?? languagesTestData,
icuPlaceholders: editOptions?.icuPlaceholders ?? true,
},
});
🤖 Prompt for AI Agents
In cypress/common/apiClient.ts around lines 29 to 36, the code uses unsafe
chained non-null assertions at
organizations.data!._embedded!.organizations![0].id which will throw if any
piece is missing or the array is empty; replace that chain with explicit runtime
checks: verify organizations.data exists, organizations.data._embedded exists,
organizations.data._embedded.organizations is an array with at least one
element, then extract the id; if any check fails, either throw a clear,
descriptive error or handle the case gracefully (return a useful error response
or use a safe fallback) so the POST body never receives an undefined path that
would crash at runtime.


client.setProjectId(project.data!.id);

await client.PUT("/v2/projects/{projectId}", {
params: {
path: {
projectId: client.getProjectId(),
},
},
body: {
icuPlaceholders: true,
useNamespaces: true,
suggestionsMode: "DISABLED",
translationProtection: "NONE",
...editOptions,
name,
},
});

await client.POST("/v2/projects/{projectId}/single-step-import-resolvable", {
params: { path: { projectId: client.getProjectId() } },
body: data,
});

return client;
}

export const DEFAULT_SCOPES = [
"keys.view",
"translations.view",
"translations.edit",
"keys.edit",
"keys.create",
"screenshots.view",
"screenshots.upload",
"screenshots.delete",
"translations.state-edit",
];

export async function createPak(client: ApiClient, scopes = DEFAULT_SCOPES) {
const apiKey = await client.POST("/v2/api-keys", {
body: { projectId: client.getProjectId(), scopes },
});

return apiKey.data!.key;
}
Comment on lines +76 to +82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Verify API response data exists before non-null assertions.

Lines 81 and 89 use non-null assertions on API response data without verifying the response succeeded. If the API call fails or returns unexpected data, this will cause runtime errors.

Apply this diff to add proper error handling:

 export async function createPak(client: ApiClient, scopes = DEFAULT_SCOPES) {
   const apiKey = await client.POST("/v2/api-keys", {
     body: { projectId: client.getProjectId(), scopes },
   });
-
-  return apiKey.data!.key;
+  if (!apiKey.data?.key) {
+    throw new Error("Failed to create API key: no key in response");
+  }
+  return apiKey.data.key;
 }

 export async function createPat(client: ApiClient) {
   const apiKey = await client.POST("/v2/pats", {
     body: { description: "e2e test pat" },
   });
-
-  return apiKey.data!.token;
+  if (!apiKey.data?.token) {
+    throw new Error("Failed to create PAT: no token in response");
+  }
+  return apiKey.data.token;
 }

Also applies to: 84-90


export async function createPat(client: ApiClient) {
const apiKey = await client.POST("/v2/pats", {
body: { description: "e2e test pat" },
});

return apiKey.data!.token;
}
19 changes: 19 additions & 0 deletions cypress/common/exampleProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { components } from "@tginternal/client";
import en from "../import-data/examples/en.json";
import fr from "../import-data/examples/fr.json";
import cs from "../import-data/examples/cs.json";
import de from "../import-data/examples/de.json";

const keys = Object.keys(en) as Array<keyof typeof en>;
export const EXAMPLE_PROJECT: components["schemas"]["SingleStepImportResolvableRequest"] =
{
keys: keys.map((name) => ({
name,
translations: {
en: { text: en[name] },
fr: { text: fr[name] },
cs: { text: cs[name] },
de: { text: de[name] },
},
})),
};
26 changes: 26 additions & 0 deletions cypress/common/languageTestData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { components } from "@tginternal/client";

export const languagesTestData: components["schemas"]["LanguageRequest"][] = [
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the import source and type safety.

The code imports components from @tginternal/client, but the relevant code snippet shows that src/client/apiSchema.generated.ts defines components as Record<string, never>, which suggests the local schema may not have the expected types.

Please confirm:

  1. Does @tginternal/client provide the correct components["schemas"]["LanguageRequest"] type?
  2. Should this import from a local generated schema instead?
  3. Are the OpenAPI types properly generated and up-to-date?

Run the following script to check the type definitions:


🏁 Script executed:

#!/bin/bash
# Check if @tginternal/client exports components with LanguageRequest schema
rg -A 10 'export.*components' node_modules/@tginternal/client --type ts 2>/dev/null || echo "@tginternal/client not found or no TypeScript files"

# Check local apiSchema for components definition
echo "--- Local apiSchema.generated.ts ---"
cat src/client/apiSchema.generated.ts | head -20

# Check if there are other generated API schema files
echo "--- Other API schema files ---"
fd 'apiSchema|api.*schema' --type f

Length of output: 577


Ensure proper type definitions for LanguageRequest

  • @tginternal/client has no TS exports; switch to importing components from src/client/apiSchema.generated.ts.
  • Regenerate your OpenAPI types (e.g. via openapi-typescript) so that components["schemas"]["LanguageRequest"] is correctly defined.
🤖 Prompt for AI Agents
In cypress/common/languageTestData.ts (lines 1-3) the code imports components
from a package that does not export TypeScript OpenAPI types; replace the import
to pull components from your generated schema file
(src/client/apiSchema.generated.ts) and update the export type to use that
generated components["schemas"]["LanguageRequest"]; then regenerate the OpenAPI
TypeScript types (e.g., run your openapi-typescript generation command and
commit the updated src/client/apiSchema.generated.ts), update any paths/tsconfig
if necessary so the generated file is resolvable, and run the TypeScript build
to confirm LanguageRequest is correctly typed.

{
name: "English",
originalName: "English",
tag: "en",
flagEmoji: "🇬🇧",
},
{
name: "French",
originalName: "French",
tag: "fr",
flagEmoji: "🇫🇷",
},
{
name: "Czech",
originalName: "Czech",
tag: "cs",
},
{
name: "German",
originalName: "German",
tag: "de",
},
];
5 changes: 2 additions & 3 deletions cypress/common/tools.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { createShortcutUrl, PluginData } from "@/web/urlConfig";

const ORIGIN = "http://localhost:3000";
const ORIGIN = "http://localhost:22224";

export const visitWithState = (data: Partial<PluginData>) => {
cy.visit(`${ORIGIN}${createShortcutUrl(data)}`);
cy.wait(100);
cy.frameLoaded("#plugin_iframe");
cy.iframeReady();
};
7 changes: 5 additions & 2 deletions cypress/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ services:
container_name: tolgee_js_e2e_server
image: tolgee/tolgee:latest
ports:
- 8080:8080
- 22223:8080
- 8092:8091
environment:
- tolgee.authentication.enabled=true
- tolgee.internal.controllerEnabled=true
- "tolgee.screenshots-url=http://localhost:8080/screenshots"
- "tolgee.screenshots-url=http://localhost:22223/screenshots"
- tolgee.authentication.needs-email-verification=true
- tolgee.authentication.registrations-allowed=true
- tolgee.internal.fake-emails-sent=true
- tolgee.authentication.initialPassword=admin
- tolgee.import.dir=/data/import-data
- tolgee.import.create-implicit-api-key=true
- tolgee.rate-limits.global-limits=false
- tolgee.rate-limits.endpoint-limits=false
- tolgee.rate-limits.authentication-limits=false
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,address=*:8091,server=y,suspend=n
volumes:
- ./import-data:/data/import-data
79 changes: 42 additions & 37 deletions cypress/e2e/copyView.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ describe("Copy view", () => {
config: PAGE_COPY,
});

cy.iframe().contains("Page copy - keys").should("be.visible");
cy.iframeBody().within(() => {
cy.get("div").contains("Page copy - keys").should("be.visible");
});
});

it("shows language if language set", () => {
visitWithState({
config: PAGE_COPY_LANGUAGE,
});

cy.iframe().contains("Page copy - cs").should("be.visible");
cy.iframeBody().within(() => {
cy.get("div").contains("Page copy - cs").should("be.visible");
});
});

it("shows unconnected node correctly", () => {
Expand All @@ -26,17 +30,17 @@ describe("Copy view", () => {
allNodes: nodes,
});

cy.iframe().contains("Test node").should("be.visible");
cy.iframeBody().within(() => {
cy.get("div").contains("Test node").should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_text")
.contains("Test node")
.should("be.visible");
cy.gcy("general_node_list_row_text")
.contains("Test node")
.should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_key")
.contains("Not connected")
.should("be.visible");
cy.gcy("general_node_list_row_key")
.contains("Not connected")
.should("be.visible");
});
});

it("shows connected node correctly", () => {
Expand All @@ -53,19 +57,19 @@ describe("Copy view", () => {
allNodes: nodes,
});

cy.iframe().contains("Test node").should("be.visible");
cy.iframeBody().within(() => {
cy.get("div").contains("Test node").should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_text")
.contains("Test node")
.should("be.visible");
cy.gcy("general_node_list_row_text")
.contains("Test node")
.should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_key")
.contains("test_key")
.should("be.visible");
cy.gcy("general_node_list_row_key")
.contains("test_key")
.should("be.visible");

cy.iframe().findDcy("general_node_list_row_namespace").should("be.empty");
cy.gcy("general_node_list_row_namespace").should("be.empty");
});
});

it("shows connected node with namespace", () => {
Expand All @@ -83,22 +87,21 @@ describe("Copy view", () => {
allNodes: nodes,
});

cy.iframe().contains("Test node").should("be.visible");
cy.iframeBody().within(() => {
cy.get("div").contains("Test node").should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_text")
.contains("Test node")
.should("be.visible");
cy.gcy("general_node_list_row_text")
.contains("Test node")
.should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_key")
.contains("test_key")
.should("be.visible");
cy.gcy("general_node_list_row_key")
.contains("test_key")
.should("be.visible");

cy.iframe()
.findDcy("general_node_list_row_namespace")
.contains("test_ns")
.should("be.visible");
cy.gcy("general_node_list_row_namespace")
.contains("test_ns")
.should("be.visible");
});
});

it("pulls changes correctly", () => {
Expand All @@ -115,10 +118,12 @@ describe("Copy view", () => {
allNodes: connectedCzech,
});

cy.iframe().contains("Na cestě").should("be.visible");
cy.iframeBody().within(() => {
cy.get("div").contains("Na cestě").should("be.visible");

cy.iframe().findDcy("copy_view_pull_button").should("be.visible").click();
cy.gcy("copy_view_pull_button").should("be.visible").click();

cy.iframe().contains("Na cestu").should("be.visible");
cy.get("div").contains("Na cestu").should("be.visible");
});
});
});
Loading