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
14 changes: 14 additions & 0 deletions packages/ios/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,20 @@ ScreenSize: ${size.width}x${size.height} (DPR: ${size.scale})
await this.wdaBackend.pressHomeButton();
}

/**
* Read the system pasteboard (clipboard). The only way to retrieve a value that an app
* exposes solely through a native "Copy" action -- e.g. a share sheet's "Copy Link" --
* since that value has no other on-screen representation to read.
*/
async getClipboardText(): Promise<string> {
return await this.wdaBackend.getPasteboard();
}

/** Write plain text to the system pasteboard (clipboard). */
async setClipboardText(text: string): Promise<void> {
await this.wdaBackend.setPasteboard(text);
}

async appSwitcher(): Promise<void> {
try {
// For iOS, use swipe up with slower/longer duration to trigger app switcher
Expand Down
51 changes: 51 additions & 0 deletions packages/ios/src/ios-webdriver-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,57 @@ export class IOSWebDriverClient extends WebDriverClient {
}
}

/**
* Read the iOS pasteboard (system clipboard) via WDA's `wda/getPasteboard`.
* Useful for values only exposed through a native "Copy" action (e.g. a share
* sheet's "Copy Link"), which have no other readable representation on screen.
* @param contentType Pasteboard content type WDA should decode as; 'plaintext' covers
* the common case (copied text/URLs).
*/
async getPasteboard(contentType = 'plaintext'): Promise<string> {
this.ensureSession();

try {
const response = await this.makeRequest(
'POST',
`/session/${this.sessionId}/wda/getPasteboard`,
{ contentType },
);
const value = response?.value;
if (!value) {
return '';
}
return Buffer.from(value, 'base64').toString('utf8');
} catch (error) {
debugIOS(`Failed to read pasteboard: ${error}`);
throw new Error(`Failed to read pasteboard: ${error}`);
}
}

/**
* Write to the iOS pasteboard (system clipboard) via WDA's `wda/setPasteboard`.
* @param text Plain text to place on the pasteboard.
* @param contentType Pasteboard content type; 'plaintext' covers the common case.
*/
async setPasteboard(text: string, contentType = 'plaintext'): Promise<void> {
this.ensureSession();

try {
await this.makeRequest(
'POST',
`/session/${this.sessionId}/wda/setPasteboard`,
{
content: Buffer.from(text, 'utf8').toString('base64'),
contentType,
},
);
debugIOS(`Wrote to pasteboard: "${text}"`);
} catch (error) {
debugIOS(`Failed to write pasteboard "${text}": ${error}`);
throw new Error(`Failed to write pasteboard: ${error}`);
}
}

async tap(x: number, y: number): Promise<void> {
this.ensureSession();

Expand Down
17 changes: 17 additions & 0 deletions packages/ios/tests/unit-test/device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe('IOSDevice', () => {
clearActiveElement: vi.fn().mockResolvedValue(true),
pressKey: vi.fn().mockResolvedValue(undefined),
pressHomeButton: vi.fn().mockResolvedValue(undefined),
getPasteboard: vi.fn().mockResolvedValue('clipboard-text'),
setPasteboard: vi.fn().mockResolvedValue(undefined),
launchApp: vi.fn().mockResolvedValue(undefined),
terminateApp: vi.fn().mockResolvedValue(undefined),
openUrl: vi.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -413,6 +415,21 @@ describe('IOSDevice', () => {
expect(mockWdaClient.swipe).toHaveBeenCalled();
});

it('should read the clipboard via the WDA pasteboard', async () => {
await device.connect();

const text = await device.getClipboardText();
expect(mockWdaClient.getPasteboard).toHaveBeenCalled();
expect(text).toBe('clipboard-text');
});

it('should write the clipboard via the WDA pasteboard', async () => {
await device.connect();

await device.setClipboardText('new-text');
expect(mockWdaClient.setPasteboard).toHaveBeenCalledWith('new-text');
});

it('should handle keyboard dismissal', async () => {
await device.connect();

Expand Down
67 changes: 66 additions & 1 deletion packages/ios/tests/unit-test/wda-backend.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,70 @@
import { DEFAULT_WDA_PORT } from '@midscene/shared/constants';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { IOSWebDriverClient } from '../../src/ios-webdriver-client';
import type { IOSWebDriverClient as IOSWebDriverClientType } from '../../src/ios-webdriver-client';

function createClientWithSession() {
const client = new IOSWebDriverClient({
port: DEFAULT_WDA_PORT,
host: 'localhost',
});
// Bypass createSession() — we only want to observe outbound HTTP calls.
(client as any).sessionId = 'session-under-test';
const makeRequest = vi
.spyOn(client as any, 'makeRequest')
.mockResolvedValue(undefined);
return { client, makeRequest };
}

describe('IOSWebDriverClient pasteboard', () => {
it('getPasteboard POSTs contentType and base64-decodes the response', async () => {
const { client, makeRequest } = createClientWithSession();
makeRequest.mockResolvedValue({
value: Buffer.from('Hello 世界', 'utf8').toString('base64'),
});

const text = await client.getPasteboard();

expect(makeRequest).toHaveBeenCalledWith(
'POST',
'/session/session-under-test/wda/getPasteboard',
{ contentType: 'plaintext' },
);
expect(text).toBe('Hello 世界');
});

it('getPasteboard returns an empty string when WDA reports no value', async () => {
const { client, makeRequest } = createClientWithSession();
makeRequest.mockResolvedValue({ value: '' });

await expect(client.getPasteboard()).resolves.toBe('');
});

it('getPasteboard rejects when the request itself fails', async () => {
const { client, makeRequest } = createClientWithSession();
makeRequest.mockRejectedValue(new Error('WDA HTTP error: 500'));

await expect(client.getPasteboard()).rejects.toThrow(
'Failed to read pasteboard',
);
});

it('setPasteboard POSTs base64-encoded content and contentType', async () => {
const { client, makeRequest } = createClientWithSession();

await client.setPasteboard('Hello 世界');

expect(makeRequest).toHaveBeenCalledWith(
'POST',
'/session/session-under-test/wda/setPasteboard',
{
content: Buffer.from('Hello 世界', 'utf8').toString('base64'),
contentType: 'plaintext',
},
);
});
});

describe('IOSWebDriverClient - Simple Tests', () => {
describe('Module Structure', () => {
it('should export IOSWebDriverClient class', async () => {
Expand Down Expand Up @@ -38,6 +101,8 @@ describe('IOSWebDriverClient - Simple Tests', () => {
'tap',
'swipe',
'typeText',
'getPasteboard',
'setPasteboard',
'pressKey',
'launchApp',
'openUrl',
Expand Down