Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c234c2e
Reorganize tests in prep for adding more
DTCurrie Nov 7, 2025
f1abc83
Reuse transport and signaling client for dialing with webrtc
DTCurrie Nov 7, 2025
892b0c0
emit disconnected event and throw on dial error
DTCurrie Nov 7, 2025
222ec7b
Handle exceptional errors when retrying connections
DTCurrie Nov 8, 2025
8d08664
Add e2e tests, fix dial attempt stacking, add testing guidelines
DTCurrie Nov 8, 2025
9502f3b
fix eslint ignores and format
DTCurrie Nov 10, 2025
2ae499d
Merge remote-tracking branch 'origin/test-reorg-and-cleanup' into reu…
DTCurrie Nov 10, 2025
8c20e5b
merge changes
DTCurrie Nov 10, 2025
ce148bf
Revert "merge changes"
DTCurrie Nov 10, 2025
3f30839
Revert "Merge remote-tracking branch 'origin/test-reorg-and-cleanup' …
DTCurrie Nov 10, 2025
db56016
format
DTCurrie Nov 10, 2025
849ec40
lint/build fix
DTCurrie Nov 10, 2025
3cd6abb
Merge branch 'reuse-transport-and-signaling-client-for-dialing-webrtc…
DTCurrie Nov 10, 2025
804664d
cleanup
DTCurrie Nov 10, 2025
4457c30
Merge branch 'dont-swallow-errors-when-dialing' into dont-retry-on-ex…
DTCurrie Nov 10, 2025
c93a7f7
merge
DTCurrie Nov 11, 2025
9ab33d2
Merge branch 'reuse-transport-and-signaling-client-for-dialing-webrtc…
DTCurrie Nov 11, 2025
5c00c76
Merge branch 'dont-swallow-errors-when-dialing' into dont-retry-on-ex…
DTCurrie Nov 11, 2025
094fa25
Merge branch 'dont-retry-on-exceptional-errors' into add-integration-…
DTCurrie Nov 11, 2025
006ba4e
cleanup and add reporting for later
DTCurrie Nov 11, 2025
f517175
cleanup
DTCurrie Nov 11, 2025
c68584c
make sure to stop viam-server
DTCurrie Nov 11, 2025
1a42197
don't throw, just emit error
DTCurrie Nov 12, 2025
ec54dfc
Merge branch 'dont-swallow-errors-when-dialing' into dont-retry-on-ex…
DTCurrie Nov 12, 2025
d90304c
actually, let's throw anyway
DTCurrie Nov 12, 2025
c2aa4d1
Merge branch 'dont-swallow-errors-when-dialing' into dont-retry-on-ex…
DTCurrie Nov 12, 2025
2ddfa59
cleanup
DTCurrie Nov 12, 2025
2c7ef2e
merge
DTCurrie Nov 12, 2025
8c7bdb7
update setup.sh to use stable for linux and mac to keep e2e consistant
DTCurrie Nov 12, 2025
0e593ec
cleanup
DTCurrie Nov 13, 2025
3cf2dea
cleanup
DTCurrie Nov 14, 2025
03d95e6
cleanup
DTCurrie Nov 14, 2025
59f18fc
fix setup.sh
DTCurrie Nov 17, 2025
c92d6c6
Merge branch 'main' of github.com:viamrobotics/viam-typescript-sdk in…
DTCurrie Nov 17, 2025
8b768d9
Merge branch 'main' of github.com:viamrobotics/viam-typescript-sdk in…
DTCurrie Nov 17, 2025
ebcb9b9
Merge branch 'reuse-transport-and-signaling-client-for-dialing-webrtc…
DTCurrie Nov 17, 2025
514e348
Merge branch 'main' of github.com:viamrobotics/viam-typescript-sdk in…
DTCurrie Nov 17, 2025
0a25c83
Merge branch 'dont-swallow-errors-when-dialing' into dont-retry-on-ex…
DTCurrie Nov 17, 2025
ef1e96f
Merge branch 'main' of github.com:viamrobotics/viam-typescript-sdk in…
DTCurrie Nov 17, 2025
2c33c65
Merge branch 'dont-retry-on-exceptional-errors' into add-integration-…
DTCurrie Nov 17, 2025
bd41450
cleanup client.spec.ts, remove tests better left for integration tests
DTCurrie Nov 17, 2025
04be205
Merge branch 'dont-retry-on-exceptional-errors' into add-integration-…
DTCurrie Nov 17, 2025
73dfbab
test component name consistency
DTCurrie Nov 17, 2025
9df9c10
verbose test output with vitest
DTCurrie Nov 17, 2025
db37430
Merge branch 'main' of github.com:viamrobotics/viam-typescript-sdk in…
DTCurrie Nov 17, 2025
cb5041d
throw when only attempting grpc and it fails, or attempting both webr…
DTCurrie Nov 17, 2025
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
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module.exports = {
'dist',
'docs',
'playwright-report',
'vitest-report',
'vitest-e2e-report',
/*
* TODO(mc, 2023-04-06): something about nested node_modules in examples
* is causing eslint to choke. Investigate workspaces as a solution
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ src/api-version.ts
*.DS_Store

playwright-report/
vitest-report/
vitest-e2e-report/
51 changes: 51 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,54 @@ make test
make lint
make format
```

## Testing

Our test suite is divided into unit tests and integration tests.

### Unit Testing

We use [Vitest](https://vitest.dev/) for unit testing to ensure that individual components of the SDK are well-tested in isolation.

**Key Principles:**

- **Location:** Unit tests are located in `src/**/__tests__`. Related test data and mocks are stored in adjacent `__fixtures__/` and `__mocks__/` directories, respectively.
- **Isolation:** Tests must be independent. Use `vi.mock()` to mock external dependencies and ensure each test case can run on its own.
- **Clarity:** Follow the Arrange-Act-Assert (AAA) pattern to structure tests clearly. Use descriptive names for `describe` blocks and test cases (e.g., `it('should do X when Y')`).

You can run all unit tests with:

```shell
make test
```

### Integration Testing

Integration tests verify the end-to-end interaction between the SDK and a live `viam-server`. We use [Vitest](https://vitest.dev/) for Node.js tests and [Playwright](https://playwright.dev/) for browser tests. All integration test code resides in the `e2e/` directory.

**Key Principles:**

- **File Naming:** Tests are separated by environment:
- `*.node.spec.ts` for Node.js-only tests.
- `*.browser.spec.ts` for browser-only tests.
- **Browser Testing:** Browser tests interact with a UI test harness (`e2e/index.html`) via a Playwright Page Object Model (`e2e/fixtures/robot-page.ts`). This ensures tests are stable and maintainable.
- **Node.js Testing:** Node.js tests interact with the SDK directly using a gRPC connection.

Before running integration tests for the first time, you must download the `viam-server` binary:

```shell
cd e2e && ./setup.sh
```

You can run the full integration test suite with:

```shell
make test-e2e
```

You can also run the Node.js and browser suites separately:

```shell
npm run e2e:node
npm run e2e:browser
```
19 changes: 12 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,20 @@ build-docs: clean-docs build-buf

.PHONY: install-playwright
install-playwright:
cd e2e && npm install
cd e2e && npx playwright install --with-deps
npm run e2e:browser-install

e2e/bin/viam-server:
bash e2e/setup.sh

.PHONY: run-e2e-server
run-e2e-server: e2e/bin/viam-server
e2e/bin/viam-server --config=./e2e/server_config.json
.PHONY: test-e2e
test-e2e: e2e/bin/viam-server install-playwright
npm run e2e:browser
npm run e2e:node

test-e2e: e2e/bin/viam-server build install-playwright
cd e2e && npm run e2e:playwright
.PHONY: test-e2e-node
test-e2e-node: e2e/bin/viam-server
npm run e2e:node

.PHONY: test-e2e-browser
test-e2e-browser: e2e/bin/viam-server install-playwright
npm run e2e:browser
23 changes: 23 additions & 0 deletions e2e/fixtures/configs/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"network": {
"fqdn": "e2e-ts-sdk",
"bind_address": ":9090"
},
"components": [
{
"name": "base1",
"type": "base",
"model": "fake"
},
{
"name": "servo1",
"type": "servo",
"model": "fake"
},
{
"name": "motor1",
"type": "motor",
"model": "fake"
}
]
}
31 changes: 31 additions & 0 deletions e2e/fixtures/configs/dial-configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { DialConf } from '../../main';

const DEFAULT_HOST = 'e2e-ts-sdk';
const DEFAULT_SERVICE_HOST = 'http://localhost:9090';
const DEFAULT_SIGNALING_ADDRESS = 'http://localhost:9090';
const DEFAULT_ICE_SERVERS = [{ urls: 'stun:global.stun.twilio.com:3478' }];

export const defaultConfig: DialConf = {
host: DEFAULT_HOST,
serviceHost: DEFAULT_SERVICE_HOST,
signalingAddress: DEFAULT_SIGNALING_ADDRESS,
iceServers: DEFAULT_ICE_SERVERS,
} as const;

export const invalidConfig: DialConf = {
host: DEFAULT_HOST,
serviceHost: 'http://invalid-host:9999',
signalingAddress: DEFAULT_SIGNALING_ADDRESS,
iceServers: DEFAULT_ICE_SERVERS,
dialTimeout: 2000,
} as const;

export const defaultNodeConfig: DialConf = {
host: DEFAULT_SERVICE_HOST,
noReconnect: true,
} as const;

export const invalidNodeConfig: DialConf = {
host: 'http://invalid-host:9999',
noReconnect: true,
} as const;
126 changes: 126 additions & 0 deletions e2e/fixtures/robot-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { test as base, type Page } from '@playwright/test';
import type { Robot, RobotClient } from '../../src/robot';
import type { ResolvedReturnType } from '../helpers/api-types';

export class RobotPage {
private readonly connectionStatusID = 'connection-status';
private readonly dialingStatusID = 'dialing-status';
private readonly connectButtonID = 'connect-btn';
private readonly disconnectButtonID = 'disconnect-btn';
private readonly outputID = 'output';

constructor(private readonly page: Page) {}

async ensureReady(): Promise<void> {
if (!this.page.url().includes('localhost:5173')) {
await this.page.goto('/');
await this.page.waitForSelector('body[data-ready="true"]');
}
}

async connect(): Promise<void> {
await this.ensureReady();
await this.page.getByTestId(this.connectButtonID).click();
await this.page.waitForSelector(
`[data-testid="${this.connectionStatusID}"]:is(:text("Connected"))`
);
}

async disconnect(): Promise<void> {
await this.page.getByTestId(this.disconnectButtonID).click();
await this.page.waitForSelector(
`[data-testid="${this.connectionStatusID}"]:is(:text("Disconnected"))`
);
}

async getConnectionStatus(): Promise<string> {
const connectionStatusEl = this.page.getByTestId(this.connectionStatusID);
const text = await connectionStatusEl.textContent();
return text ?? 'Unknown';
}

async waitForDialing(): Promise<void> {
await this.page.waitForSelector(
`[data-testid="${this.dialingStatusID}"]:not(:empty)`,
{ timeout: 5000 }
);
}

async waitForFirstDialingAttempt(): Promise<void> {
await this.page.waitForFunction(
(testId: string) => {
const el = document.querySelector(`[data-testid="${testId}"]`);
const text = el?.textContent ?? '';
const match = text.match(/attempt (?<attemptNumber>\d+)/u);
if (!match?.groups) {
return false;
}
const attemptNumber = Number.parseInt(
match.groups.attemptNumber ?? '0',
10
);
return attemptNumber === 1;
},
this.dialingStatusID,
{ timeout: 10_000 }
);
}

async waitForSubsequentDialingAttempts(): Promise<void> {
await this.page.waitForFunction(
(testId: string) => {
const el = document.querySelector(`[data-testid="${testId}"]`);
const text = el?.textContent ?? '';
const match = text.match(/attempt (?<attemptNumber>\d+)/u);
if (!match?.groups) {
return false;
}
const attemptNumber = Number.parseInt(
match.groups.attemptNumber ?? '0',
10
);
return attemptNumber > 1;
},
this.dialingStatusID,
{ timeout: 10_000 }
);
}

async getDialingStatus(): Promise<string> {
const dialingStatusEl = this.page.getByTestId(this.dialingStatusID);
const text = await dialingStatusEl.textContent();
return text ?? '';
}

async getOutput<T extends Robot, K extends keyof T>(): Promise<
ResolvedReturnType<T[K]>
> {
// Wait for the output to be updated by checking for the data-has-output attribute
await this.page.waitForSelector(
`[data-testid="${this.outputID}"][data-has-output="true"]`,
{ timeout: 30_000 }
);
const outputEl = this.page.getByTestId(this.outputID);
const text = await outputEl.textContent();
return JSON.parse(text ?? '{}') as ResolvedReturnType<T[K]>;
}

async clickButton(testId: string): Promise<void> {
await this.page.click(`[data-testid="${testId}"]`);
}

async clickRobotAPIButton(apiName: keyof RobotClient): Promise<void> {
await this.page.click(`[data-robot-api="${apiName}"]`);
}

getPage(): Page {
return this.page;
}
}

export const withRobot = base.extend<{ robotPage: RobotPage }>({
robotPage: async ({ page }, use) => {
const robotPage = new RobotPage(page);
await use(robotPage);
},
});
9 changes: 9 additions & 0 deletions e2e/helpers/api-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never;

export type ResolvedReturnType<T> = T extends (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
) => Promise<infer R>
? R
: never;
Loading