vorteil/direktiv

View on GitHub
ui/e2e/explorer/route/index.spec.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { createNamespace, deleteNamespace } from "e2e/utils/namespace";
import { createRouteYaml, removeLines } from "./utils";
import { expect, test } from "@playwright/test";

import { createFile } from "e2e/utils/files";

let namespace = "";

test.beforeEach(async () => {
  namespace = await createNamespace();
});

test.afterEach(async () => {
  await deleteNamespace(namespace);
  namespace = "";
});

test("it is possible to create a basic route file", async ({ page }) => {
  /* prepare data */
  const filename = "myroute.yaml";

  const expectedYaml = createRouteYaml({
    path: "path",
    timeout: 3000,
    methods: ["GET", "POST"],
    plugins: {
      target: `
    type: instant-response
    configuration:
        status_code: 200`,
    },
  });

  /* visit page */
  await page.goto(`/n/${namespace}/explorer/tree`, {
    waitUntil: "networkidle",
  });
  await expect(
    page.getByTestId("breadcrumb-namespace"),
    "it navigates to the test namespace in the explorer"
  ).toHaveText(namespace);

  /* create route */
  await page.getByRole("button", { name: "New" }).first().click();
  await page.getByRole("menuitem", { name: "Gateway" }).click();
  await page.getByRole("button", { name: "New Route" }).click();

  await expect(page.getByRole("button", { name: "Create" })).toBeDisabled();
  await page.getByPlaceholder("route-name.yaml").fill(filename);
  await page.getByRole("button", { name: "Create" }).click();

  /**
   * close the toast, which covers the save button and prevents
   * us from clicking it (makes this test 4 seconds faster)
   */
  await page.getByTestId("toast-close").click();

  await expect(
    page,
    "it creates the route file and opens it in the explorer"
  ).toHaveURL(`/n/${namespace}/explorer/endpoint/${filename}`);

  /* fill out form */
  await page.getByLabel("path").fill("path");
  await page.getByLabel("timeout").fill("3000");
  await page.getByLabel("GET").click();
  await page.getByLabel("POST").click();

  /* try to save incomplete form */
  await page.getByRole("button", { name: "Save" }).click();

  await expect(
    page.getByText("plugins : this field is invalid"),
    "it can not save the route without a valid target plugin"
  ).toBeVisible();

  await page.getByRole("button", { name: "set target plugin" }).click();

  /* add an empty instant response plugin */
  await page.getByRole("combobox").click();
  await page.getByLabel("Instant Response").click();
  await page.getByRole("button", { name: "Save" }).click();

  /* check editor content */
  const editor = page.locator(".lines-content");
  await expect(
    editor,
    "all entered data is represented in the editor preview"
  ).toContainText(expectedYaml, { useInnerText: true });

  /* note: saving the plugin should have saved the whole file. */
  await expect(
    page.getByText("unsaved changes"),
    "it does not render a hint that there are unsaved changes"
  ).not.toBeVisible();

  /* reload */
  await page.reload({ waitUntil: "networkidle" });

  await expect(
    editor,
    "after reloading, the entered data is still in the editor preview"
  ).toContainText(expectedYaml, { useInnerText: true });

  page.getByRole("link", { name: "Open Logs" }).click();

  await expect(
    page,
    "when the open logs link is clicked, page should navigate to the route detail page"
  ).toHaveURL(`/n/${namespace}/gateway/routes/${filename}`);
});

test("it is possible to add plugins to a route file", async ({ page }) => {
  /* prepare data */
  const filename = "myroute.yaml";
  const editor = page.locator(".lines-content");

  type CreateRouteYamlParam = Parameters<typeof createRouteYaml>[0];
  const minimalRouteConfig: Omit<CreateRouteYamlParam, "plugins"> = {
    path: "path",
    timeout: 3000,
    methods: ["GET", "POST"],
  };

  const basicTargetPlugin = `
    type: instant-response
    configuration:
      status_code: 200`;

  const initialRouteYaml = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
    },
  });

  await createFile({
    namespace,
    name: filename,
    type: "endpoint",
    yaml: initialRouteYaml,
  });

  await page.goto(`/n/${namespace}/explorer/endpoint/${filename}`, {
    waitUntil: "networkidle",
  });

  /* configure inbound plugin: ACL */
  await page.getByRole("button", { name: "add inbound plugin" }).click();
  await page.getByRole("combobox").click();
  await page.getByLabel("Access control list (acl)").click();

  await page
    .locator("fieldset")
    .filter({ hasText: "Allow Groups (optional)" })
    .getByPlaceholder("Enter a group")
    .fill("allow this group 1");

  /* submit via enter */
  await page
    .locator("fieldset")
    .filter({ hasText: "Allow Groups (optional)" })
    .getByPlaceholder("Enter a group")
    .press("Enter");

  await page
    .locator("fieldset")
    .filter({ hasText: "Allow Groups (optional)" })
    .getByPlaceholder("Enter a group")
    .nth(1)
    .fill("allow this group 2");

  /* submit via button */
  await page
    .locator("fieldset")
    .filter({ hasText: "Allow Groups (optional)" })
    .getByRole("button")
    .nth(1)
    .click();

  await page.getByRole("button", { name: "Save" }).click();

  /* configure inbound plugin: Request Convert */
  await page.getByRole("button", { name: "add inbound plugin" }).click();
  await page.getByRole("combobox").click();
  await page.getByLabel("Request Convert").click();
  await page.getByText("Omit Queries").click();
  await page.getByText("Omit Consumer").click();
  await page.getByRole("button", { name: "Save" }).click();

  /* check editor content */
  const inboundPluginsBeforeSorting = `
    - type: acl
      configuration:
        allow_groups:
          - allow this group 1
          - allow this group 2
        deny_groups: []
        allow_tags: []
        deny_tags: []
    - type: request-convert
      configuration:
        omit_headers: false
        omit_queries: true
        omit_body: false
        omit_consumer: true`;

  const inboundPluginsAfterSorting = `
    - type: request-convert
      configuration:
        omit_headers: false
        omit_queries: true
        omit_body: false
        omit_consumer: true
    - type: acl
      configuration:
        allow_groups:
          - allow this group 1
          - allow this group 2
        deny_groups: []
        allow_tags: []
        deny_tags: []`;

  let expectedEditorContent = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
      inbound: inboundPluginsBeforeSorting,
    },
  });

  /**
   * Note: the editor only shows a limited amount of lines, The Editor uses a
   * virtualized list to render the content. This means that the invisible content
   * is not even rendered in the DOM. So from now on we have to crop some lines
   * in our assertions to make them pass. This is not a big problem, because we
   * already tested the upper part of the file in the previous test.
   *
   * We will scroll the editor to the very bottom, now. The editor will automatically
   * keep that scroll position when we change the content.
   */
  await page.evaluate(() => {
    document
      .querySelector(".monaco-editor .monaco-scrollable-element")
      ?.scrollBy(0, 100000000);
  });

  await expect(
    editor,
    "the inbound plugins are represented in the editor preview"
  ).toContainText(removeLines(expectedEditorContent, 4, "top"), {
    useInnerText: true,
  });

  /* change sorting of inbound plugins */
  await page
    .getByRole("row", { name: "Access control list (acl)" })
    .getByRole("button")
    .click();
  await page.getByRole("button", { name: "Move down" }).click();

  expectedEditorContent = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
      inbound: inboundPluginsAfterSorting,
    },
  });

  await expect(
    editor,
    "the new inbound plugin order is represented in the editor preview"
  ).toContainText(removeLines(expectedEditorContent, 4, "top"), {
    useInnerText: true,
  });

  /* configure outbound plugin: JavaScript */
  await page.getByRole("button", { name: "add outbound plugin" }).click();
  await page.getByRole("combobox").click();
  await page.getByLabel("JavaScript").click();
  await page.getByRole("textbox").fill("// execute some JavaScript here");
  await page.getByRole("button", { name: "Save" }).click();

  /* check editor content */
  const outboundPlugins = `
    - type: js-outbound
      configuration:
        script: // execute some JavaScript here`;

  expectedEditorContent = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
      inbound: inboundPluginsAfterSorting,
      outbound: outboundPlugins,
    },
  });

  await expect(
    editor,
    "the outbound plugin is represented in the editor preview"
  ).toContainText(removeLines(expectedEditorContent, 7, "top"), {
    useInnerText: true,
  });

  /* configure auth plugin: Github Webhook */
  await page.getByRole("button", { name: "add auth plugin" }).click();
  await page.getByRole("combobox").click();
  await page.getByLabel("Github Webhook").click();
  await page.getByLabel("secret").fill("my github secret");
  await page.getByRole("button", { name: "Save" }).click();

  /* check editor content */
  const authPlugins = `
    - type: github-webhook-auth
      configuration:
        secret: my github secret`;

  expectedEditorContent = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
      inbound: inboundPluginsAfterSorting,
      outbound: outboundPlugins,
      auth: authPlugins,
    },
  });

  await expect(
    editor,
    "the auth plugin is represented in the editor preview"
  ).toContainText(removeLines(expectedEditorContent, 10, "top"), {
    useInnerText: true,
  });

  /* note: saving the plugin should have saved the whole file. */
  await expect(
    page.getByText("unsaved changes"),
    "it does not render a hint that there are unsaved changes"
  ).not.toBeVisible();

  /* reload */
  await page.reload({ waitUntil: "networkidle" });
  await expect(
    editor,
    "after reloading, the entered data is still in the editor preview"
  ).toContainText(removeLines(expectedEditorContent, 9, "bottom"), {
    useInnerText: true,
  });

  /* delete all optional plugins */
  await page
    .getByRole("row", { name: "Access control list (acl)" })
    .getByRole("button")
    .click();
  await page.getByRole("button", { name: "Delete" }).click();

  await page
    .getByRole("row", { name: "Request convert" })
    .getByRole("button")
    .click();
  await page.getByRole("button", { name: "Delete" }).click();

  await page
    .getByRole("row", { name: "Javascript" })
    .getByRole("button")
    .click();
  await page.getByRole("button", { name: "Delete" }).click();

  await page
    .getByRole("row", { name: "Github Webhook" })
    .getByRole("button")
    .click();
  await page.getByRole("button", { name: "Delete" }).click();

  expectedEditorContent = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
    },
  });

  await expect(
    editor,
    "the deleted plugins are also represented in the editor preview"
  ).toContainText(expectedEditorContent, {
    useInnerText: true,
  });
});

test("it blocks navigation when there are unsaved changes", async ({
  page,
}) => {
  let dialogTriggered = false;

  page.on("dialog", async (dialog) => {
    dialogTriggered = true;
    return dialog.dismiss();
  });

  /* prepare data */
  const filename = "formatroute.yaml";

  type CreateRouteYamlParam = Parameters<typeof createRouteYaml>[0];
  const minimalRouteConfig: Omit<CreateRouteYamlParam, "plugins"> = {
    path: "path",
    timeout: 3000,
    methods: ["GET", "POST"],
  };

  const basicTargetPlugin = `
    type: instant-response
    configuration:
      status_code: 200`;

  const initialRouteYaml = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
    },
  });

  await createFile({
    namespace,
    name: filename,
    type: "endpoint",
    yaml: initialRouteYaml,
  });

  /* visit page */
  await page.goto(`/n/${namespace}/explorer/endpoint/${filename}`, {
    waitUntil: "networkidle",
  });

  await page.getByLabel("path").fill("new_path");

  await expect(
    page.getByText("unsaved changes"),
    "it renders a hint that there are unsaved changes"
  ).toBeVisible();

  await page.getByRole("link", { name: "Monitoring" }).click();

  await expect(
    dialogTriggered,
    "it triggers a confirmation dialogue"
  ).toBeTruthy();

  await expect(page, "it does not navigate away from the page").toHaveURL(
    `/n/${namespace}/explorer/endpoint/${filename}`
  );
});

test("it does not block navigation when only formatting has changed", async ({
  page,
}) => {
  /**
   * In this test, the original file has non-standard formatting. This can happen
   * when a yaml file was created on an earlier direktiv version or using a
   * third-party editor with different formatting rules. When the file is loaded
   * into the form and the form's values are converted back to yaml, this formatting
   * is updated. This test ensures that these changes are not regarded as "unsaved
   * changes", blocking navigation away from the page.
   */
  let dialogTriggered = false;

  page.on("dialog", async (dialog) => {
    dialogTriggered = true;
    return dialog.dismiss();
  });

  /* prepare data */
  const filename = "formatroute.yaml";

  type CreateRouteYamlParam = Parameters<typeof createRouteYaml>[0];
  const minimalRouteConfig: Omit<CreateRouteYamlParam, "plugins"> = {
    path: "path",
    timeout: 3000,
    methods: ["GET", "POST"],
  };

  const basicTargetPlugin = `
    type: "instant-response"
    configuration:
      status_code: 200`;

  const initialRouteYaml = createRouteYaml({
    ...minimalRouteConfig,
    plugins: {
      target: basicTargetPlugin,
    },
  });

  await createFile({
    namespace,
    name: filename,
    type: "endpoint",
    yaml: initialRouteYaml,
  });

  /* visit page */
  await page.goto(`/n/${namespace}/explorer/endpoint/${filename}`, {
    waitUntil: "networkidle",
  });

  const formattedText = page
    .getByRole("code")
    .locator("div")
    .filter({ hasText: "type: instant-response" })
    .nth(4);

  await expect(
    formattedText,
    "it has updated the formatting (removed quotes)"
  ).toBeVisible();

  await expect(
    page.getByText("unsaved changes"),
    "it does not render a hint that there are unsaved changes"
  ).not.toBeVisible();

  await page.getByRole("link", { name: "Monitoring" }).click();

  await expect(
    dialogTriggered,
    "it does not trigger a warning dialogue"
  ).toBeFalsy();

  await expect(
    page.getByRole("heading", { name: "Monitoring", exact: true }),
    "it is possible to leave the route"
  ).toBeVisible();
});