From 1235ca5928bf05f6a31fafcf22a5dc327d9a2cc3 Mon Sep 17 00:00:00 2001 From: iphydf Date: Tue, 27 Feb 2024 00:43:11 +0000 Subject: [PATCH] test: Add some UI integration tests (end-to-end). --- .github/workflows/ci.yml | 8 ++ .restyled.yaml | 1 + test/.gitignore | 1 + test/Dockerfile | 43 +++++++ test/README.md | 9 ++ test/__snapshots__/toxic.test.ts.snap | 177 ++++++++++++++++++++++++++ test/package.json | 6 + test/run | 19 +++ test/toxic.test.ts | 65 ++++++++++ 9 files changed, 329 insertions(+) create mode 100644 test/.gitignore create mode 100644 test/Dockerfile create mode 100644 test/README.md create mode 100644 test/__snapshots__/toxic.test.ts.snap create mode 100644 test/package.json create mode 100755 test/run create mode 100644 test/toxic.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edd080a2d..d62b9f72c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,3 +126,11 @@ jobs: - name: Print log run: cat /__w/toxic/toxic/infer-out/report.txt + + integration-test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run integration tests + run: test/run diff --git a/.restyled.yaml b/.restyled.yaml index 2c2455b8a..5d2a13e68 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -7,3 +7,4 @@ restylers: include: - "**/*.cc" - "**/*.hh" + - prettier diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..671735509 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +.tui-test/ diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 000000000..c46295653 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,43 @@ +FROM alpine:3.19.0 + +RUN ["apk", "add", "--no-cache", \ + "bash", \ + "cmake", \ + "curl-dev", \ + "g++", \ + "gcc", \ + "git", \ + "libconfig-dev", \ + "libsodium-dev", \ + "libvpx-dev", \ + "linux-headers", \ + "make", \ + "ncurses-dev", \ + "opus-dev", \ + "pkgconfig", \ + "samurai", \ + "yarn"] + +WORKDIR /build +RUN yarn add --dev @microsoft/tui-test +ENV PATH=$PATH:/build/node_modules/.bin + +WORKDIR /build +RUN git clone --depth=1 --recursive https://github.com/TokTok/c-toxcore /build/c-toxcore \ + && cmake -GNinja -B/build/c-toxcore/_build -H/build/c-toxcore \ + -DBOOTSTRAP_DAEMON=OFF \ + -DENABLE_STATIC=OFF \ + -DMUST_BUILD_TOXAV=ON \ + && cmake --build /build/c-toxcore/_build --target install + +WORKDIR /build/toxic +COPY Makefile /build/toxic/ +COPY cfg /build/toxic/cfg/ +COPY misc /build/toxic/misc/ +COPY sounds /build/toxic/sounds/ +COPY src /build/toxic/src/ +ENV CFLAGS="-D_GNU_SOURCE -Werror" +RUN make "-j$(nproc)" install + +WORKDIR /build/toxic/test +ENTRYPOINT ["tui-test"] diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..f1412f004 --- /dev/null +++ b/test/README.md @@ -0,0 +1,9 @@ +# Integration test suite + +`test/run` builds a docker image with tui-test and toxic in it and then runs +integration tests, i.e. launch the toxic binary in a virtual terminal and +mechanically interacts with it via stdin/stdout I/O. At the end of a test, it +may take a "screenshot" (black and white copy of the current screen) and +compare it against goldens, similar to Jest Snapshot Testing. + +To update the goldens (when changing something in the UI), run `test/run -u`. diff --git a/test/__snapshots__/toxic.test.ts.snap b/test/__snapshots__/toxic.test.ts.snap new file mode 100644 index 000000000..0b7c6a145 --- /dev/null +++ b/test/__snapshots__/toxic.test.ts.snap @@ -0,0 +1,177 @@ +// TUI Test Snapshot v1 + +exports[`global help window works 1`] = String.raw` +╭──────────────────────────────────────────────────────────────────────────────────────────╮ +│┌──────────────────────────────────────────────────────────────────────────────┐ │ +││Global Commands: │ │ +││ /add : Add contact with optional message │ │ +││ /accept : Accept friend request │ │ +││ /avatar : Set an avatar (leave path empty to unset) │ │ +││ /color : Change the colour of the focused window's name │ │ +││ /conference : Create a conference where type: text | audio │ │ +││ /connect : Manually connect to a DHT node │ │ +││ /decline : Decline friend request │ │ +││ /requests : List pending friend requests │ │ +││ /status : Set status (Online, Busy, Away) │ │ +││ /note : Set a personal note │ │ +││ /nick : Set your global name (doesn't affect groups) │ │ +││ /nospam : Change part of your Tox ID to stop spam │ │ +││ /log | : Enable/disable logging │ │ +││ /myid : Print your Tox ID │ │ +││ /group : Create a new group chat │ │ +││ /join : Join a public groupchat using a Chat ID │ │ +││ /game : Play a game │ │ +││ /clear : Clear window history │ │ +││ /close : Close the current chat window │ │ +││ /quit or /exit : Exit Toxic │ │ +││ │ │ +││ main menu | exit │ │ +│└──────────────────────────────────────────────────────────────────────────────┘ │ +╰──────────────────────────────────────────────────────────────────────────────────────────╯ +`; + +exports[`help window can be exited 1`] = String.raw` +╭──────────────────────────────────────────────────────────────────────────────────────────╮ +│ [Offline] Toxic User │ +│ _____ _____ _____ ____ │ +│ |_ _/ _ \ \/ /_ _/ ___| │ +│ | || | | \ / | | | │ +│ | || |_| / \ | | |___ │ +│ |_| \___/_/\_\___\____| v.0.14.1 │ +│ │ +│Welcome to Toxic, a free, open source Tox-based instant messaging client. │ +│Type "/help" for assistance. Further help may be found via the man page. │ +│ │ +│Avatar has been unset. │ +│$ /help │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ [Home] [Contacts] │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────╯ +`; + +exports[`help window works 1`] = String.raw` +╭──────────────────────────────────────────────────────────────────────────────────────────╮ +│ [Offline] Toxic User │ +│ _____ _____ _____ ____ │ +│ |_ _/ _ \ \/ /_ _/ ___| │ +│ ┌────────────────────────┐ │ +│ │ Help Menu │ │ +│ │global commands │v.0.14.1 │ +│ │chat commands │ │ +│Wel│conference commands │n source Tox-based instant messaging client. │ +│Typ│groupchat commands │Further help may be found via the man page. │ +│ │friendlist controls │ │ +│Ava│key bindings │ │ +│$ /└────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ [Home] [Contacts] │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────╯ +`; + +exports[`terminal resize works 1`] = String.raw` +╭────────────────────────────────────────────────────────────╮ +│ [Offline] Toxic User │ +│ _____ _____ _____ ____ │ +│ |_ _/ _ \ \/ /_ _/ ___| │ +│ ┌────────────────────────┐ │ +│ │ Help Menu │ │ +│ │global commands │v.0.14.1 │ +│ │chat commands │ │ +│Wel│conference commands │n source Tox-based instant │ +│mes│groupchat commands │ │ +│Typ│friendlist controls │Further help may be found via │ +│the│key bindings │ │ +│ └────────────────────────┘ │ +│Avatar has been unset. │ +│$ /help │ +│ │ +│ │ +│ │ +│ │ +│ [Home] [Contacts] │ +│ │ +╰────────────────────────────────────────────────────────────╯ +`; + +exports[`toxic --help shows usage message 1`] = String.raw` +╭──────────────────────────────────────────────────────────────────────────────────────────╮ +│> toxic --help │ +│usage: toxic [OPTION] [FILE ...] │ +│ -4, --ipv4 Force IPv4 connection │ +│ -b, --debug Enable stderr for debugging │ +│ -c, --config Use specified config file │ +│ -d, --default-locale Use default POSIX locale │ +│ -e, --encrypt-data Encrypt an unencrypted data file │ +│ -f, --file Use specified data file │ +│ -h, --help Show this message and exit │ +│ -l, --logging Enable toxcore logging: Requires [log_path | stderr] │ +│ -L, --no-lan Disable local discovery │ +│ -n, --nodes Use specified DHTnodes file │ +│ -o, --noconnect Do not connect to the DHT network │ +│ -p, --SOCKS5-proxy Use SOCKS5 proxy: Requires [IP] [port] │ +│ -P, --HTTP-proxy Use HTTP proxy: Requires [IP] [port] │ +│ -r, --namelist Use specified name lookup server list │ +│ -t, --force-tcp Force toxic to use a TCP connection (use with proxies) │ +│ -T, --tcp-server Act as a TCP relay server: Requires [port] │ +│ -u, --unencrypt-data Unencrypt an encrypted data file │ +│ -v, --version Print the version │ +│> │ +│ │ +│ │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────╯ +`; + +exports[`toxic shows intro message 1`] = String.raw` +╭──────────────────────────────────────────────────────────────────────────────────────────╮ +│ [Offline] Toxic User | Toxing on Toxic │ +│ _____ _____ _____ ____ │ +│ |_ _/ _ \ \/ /_ _/ ___| │ +│ | || | | \ / | | | │ +│ | || |_| / \ | | |___ │ +│ |_| \___/_/\_\___\____| v.0.14.1 │ +│ │ +│Welcome to Toxic, a free, open source Tox-based instant messaging client. │ +│Type "/help" for assistance. Further help may be found via the man page. │ +│ │ +│Avatar has been unset. │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ [Home] [Contacts] │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────╯ +`; + diff --git a/test/package.json b/test/package.json new file mode 100644 index 000000000..c490e27de --- /dev/null +++ b/test/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "devDependencies": { + "@microsoft/tui-test": "^0.0.1" + } +} diff --git a/test/run b/test/run new file mode 100755 index 000000000..945a37d2e --- /dev/null +++ b/test/run @@ -0,0 +1,19 @@ +#!/bin/sh + +set -eux + +docker build -t toxchat/toxic:test -f test/Dockerfile . +if [ -t 1 ]; then + docker run \ + --volume "$PWD/test:/build/toxic/test" \ + --tmpfs /build/toxic/test/.tui-test \ + --rm \ + -it \ + toxchat/toxic:test "$@" +else + docker run \ + --volume "$PWD/test:/build/toxic/test" \ + --tmpfs /build/toxic/test/.tui-test \ + --rm \ + toxchat/toxic:test "$@" +fi diff --git a/test/toxic.test.ts b/test/toxic.test.ts new file mode 100644 index 000000000..43e602a25 --- /dev/null +++ b/test/toxic.test.ts @@ -0,0 +1,65 @@ +import { test, expect, Shell } from "@microsoft/tui-test"; + +test.use({ shell: Shell.Bash, rows: 25, columns: 90 }); + +const fast = { timeout: 100 }; +const slow = { timeout: 1000 }; + +async function startToxic(terminal: Shell.Terminal) { + terminal.write("toxic\r"); + return expect(terminal.getByText("Welcome to Toxic")).toBeVisible(slow); +} + +test.describe("toxic", () => { + test("toxic --help shows usage message", async ({ terminal }) => { + terminal.write("toxic --help\r"); + await expect(terminal.getByText("usage: toxic")).toBeVisible(fast); + await expect(terminal).toMatchSnapshot(); + }); + + test("toxic shows intro message", async ({ terminal }) => { + terminal.write("toxic\r"); + await expect( + terminal.getByText("Would you like to encrypt it") + ).toBeVisible(fast); + terminal.write("n\r"); + await expect(terminal.getByText("Welcome to Toxic")).toBeVisible(slow); + await expect(terminal).toMatchSnapshot(); + }); + + test("help window works", async ({ terminal }) => { + await startToxic(terminal); + terminal.write("/help\r"); + await expect(terminal.getByText("Help Menu")).toBeVisible(fast); + await expect(terminal).toMatchSnapshot(); + }); + + test("global help window works", async ({ terminal }) => { + await startToxic(terminal); + terminal.write("/help\r"); + await expect(terminal.getByText("Help Menu")).toBeVisible(fast); + terminal.write("g"); + await expect(terminal.getByText("Global Commands")).toBeVisible(fast); + await expect(terminal).toMatchSnapshot(); + }); + + test("help window can be exited", async ({ terminal }) => { + await startToxic(terminal); + terminal.write("/help\r"); + await expect(terminal.getByText("Help Menu")).toBeVisible(fast); + terminal.write("x"); + await expect(terminal.getByText("Help Menu")).not.toBeVisible(fast); + await expect(terminal).toMatchSnapshot(); + }); + + test("terminal resize works", async ({ terminal }) => { + await startToxic(terminal); + terminal.write("/help\r"); + await expect(terminal.getByText("Help Menu")).toBeVisible(fast); + terminal.resize(60, 20); + await expect(terminal.getByText("Help Menu")).toBeVisible(fast); + // Wait for one of the last things to be drawn before screenshotting. + await expect(terminal.getByText("[Contacts]")).toBeVisible(fast); + await expect(terminal).toMatchSnapshot(); + }); +});