Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8d9568c

Browse files
authoredMay 23, 2025··
fix: handle ws browser error; add basic browser testing (#1428)
1 parent 288aff4 commit 8d9568c

12 files changed

+8265
-270
lines changed
 

‎README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key
8686
})
8787
```
8888

89+
## Testing
90+
91+
### Unit Testing
92+
93+
```bash
94+
pnpm test
95+
```
96+
97+
### Integration Testing
98+
99+
```bash
100+
supabase start
101+
pnpm run test:integration
102+
```
103+
89104
## Badges
90105

91106
[![Coverage Status](https://coveralls.io/repos/github/supabase/supabase-js/badge.svg?branch=master)](https://coveralls.io/github/supabase/supabase-js?branch=master)

‎jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ const config: Config.InitialOptions = {
1414
'!**/vendor/**',
1515
'!**/vendor/**',
1616
],
17+
testPathIgnorePatterns: ['test/integration.browser.test.ts'],
1718
}
1819
export default config

‎package-lock.json

Lines changed: 116 additions & 267 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
"build:umd": "webpack",
3030
"types-generate": "dts-gen -m '@supabase/supabase-js' -s",
3131
"test": "run-s test:types test:run",
32-
"test:run": "jest --runInBand",
33-
"test:coverage": "jest --runInBand --coverage",
32+
"test:run": "jest --runInBand --detectOpenHandles",
33+
"test:integration": "jest --runInBand test/integration.test.ts",
34+
"test:coverage": "jest --runInBand --coverage --testPathIgnorePatterns=\"test/integration.test.ts\"",
3435
"test:db": "cd infra/db && docker-compose down && docker-compose up -d && sleep 5",
3536
"test:watch": "jest --watch --verbose false --silent false",
3637
"test:clean": "cd infra/db && docker-compose down",
@@ -44,7 +45,7 @@
4445
"@supabase/functions-js": "2.4.4",
4546
"@supabase/node-fetch": "2.6.15",
4647
"@supabase/postgrest-js": "1.19.4",
47-
"@supabase/realtime-js": "2.11.2",
48+
"@supabase/realtime-js": "2.11.9-next.1",
4849
"@supabase/storage-js": "2.7.1"
4950
},
5051
"devDependencies": {

‎pnpm-lock.yaml

Lines changed: 7861 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎supabase/.branches/_current_branch

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
main

‎supabase/.temp/cli-latest

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v2.22.12
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Create todos table
2+
CREATE TABLE IF NOT EXISTS public.todos (
3+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4+
task TEXT NOT NULL,
5+
is_complete BOOLEAN NOT NULL DEFAULT FALSE,
6+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
7+
);
8+
9+
-- Set up Row Level Security (RLS)
10+
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;
11+
12+
-- Create policies
13+
CREATE POLICY "Allow anonymous access to todos" ON public.todos
14+
FOR ALL
15+
TO anon
16+
USING (true)
17+
WITH CHECK (true);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
create policy "authenticated can read all messages on topic"
2+
on "realtime"."messages"
3+
for select
4+
to authenticated
5+
using ( realtime.topic() like '%channel%' );
6+
7+
create policy "authenticated can insert messages on topic"
8+
on "realtime"."messages"
9+
for insert
10+
to authenticated
11+
with check (realtime.topic() like '%channel%');

‎supabase/seed/seed_todos.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Seed data for todos table
2+
INSERT INTO public.todos (task, is_complete)
3+
VALUES
4+
('Buy groceries', false),
5+
('Complete project report', true),
6+
('Call mom', false),
7+
('Schedule dentist appointment', false),
8+
('Pay bills', true);

‎test/integration.browser.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { serve } from 'https://deno.land/std@0.192.0/http/server.ts'
2+
import { assertEquals } from 'https://deno.land/std@0.224.0/testing/asserts.ts'
3+
import { describe, it, beforeAll, afterAll } from 'https://deno.land/std@0.224.0/testing/bdd.ts'
4+
import { Browser, Page, launch } from 'npm:puppeteer@24.9.0'
5+
import { sleep } from 'https://deno.land/x/sleep/mod.ts'
6+
// Run the UMD build before serving the page
7+
const stderr = 'inherit'
8+
const ac = new AbortController()
9+
10+
let browser: Browser
11+
let page: Page
12+
13+
const port = 8000
14+
const content = `<html>
15+
<body>
16+
<div id="output"></div>
17+
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
18+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
19+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
20+
<script src="http://localhost:${port}/supabase.js"></script>
21+
22+
<script type="text/babel" data-presets="env,react">
23+
const SUPABASE_URL = 'http://127.0.0.1:54321'
24+
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0'
25+
const supabase = supabase.createClient(SUPABASE_URL, ANON_KEY)
26+
const App = (props) => {
27+
const [realtimeStatus, setRealtimeStatus] = React.useState(null)
28+
const channel = supabase.channel('realtime:public:todos')
29+
React.useEffect(() => {
30+
if (channel.state === 'closed') {
31+
channel.subscribe((status) => { if (status === 'SUBSCRIBED') setRealtimeStatus(status) })
32+
}
33+
}, [])
34+
if (realtimeStatus) {
35+
return <div id='realtime_status'>{realtimeStatus}</div>
36+
} else {
37+
return <div></div>
38+
}
39+
}
40+
ReactDOM.render(<App />, document.getElementById('output'));
41+
</script>
42+
</body>
43+
</html>
44+
`
45+
46+
beforeAll(async () => {
47+
await new Deno.Command('supabase', { args: ['start'], stderr }).output()
48+
await new Deno.Command('pnpm', { args: ['install'], stderr }).output()
49+
await new Deno.Command('pnpm', { args: ['build:umd', '--mode', 'production'], stderr }).output()
50+
51+
await new Deno.Command('npx', {
52+
args: ['puppeteer', 'browsers', 'install', 'chrome'],
53+
stderr,
54+
}).output()
55+
56+
serve(
57+
async (req: any) => {
58+
if (req.url.endsWith('supabase.js')) {
59+
const file = await Deno.readFile('./dist/umd/supabase.js')
60+
61+
return new Response(file, {
62+
headers: { 'content-type': 'application/javascript' },
63+
})
64+
}
65+
return new Response(content, {
66+
headers: {
67+
'content-type': 'text/html',
68+
'cache-control': 'no-cache',
69+
},
70+
})
71+
},
72+
{ signal: ac.signal, port: port }
73+
)
74+
})
75+
76+
afterAll(async () => {
77+
await ac.abort()
78+
await page.close()
79+
await browser.close()
80+
await sleep(1)
81+
})
82+
83+
describe('Realtime integration test', () => {
84+
beforeAll(async () => {
85+
browser = await launch()
86+
page = await browser.newPage()
87+
})
88+
89+
it('connects to realtime', async () => {
90+
await page.goto('http://localhost:8000')
91+
await page.waitForSelector('#realtime_status', { timeout: 2000 })
92+
const realtimeStatus = await page.$eval('#realtime_status', (el) => el.innerHTML)
93+
assertEquals(realtimeStatus, 'SUBSCRIBED')
94+
})
95+
})

‎test/integration.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { createClient, RealtimeChannel, SupabaseClient } from '../src/index'
2+
3+
// These tests assume that a local Supabase server is already running
4+
// Start a local Supabase instance with 'supabase start' before running these tests
5+
describe('Supabase Integration Tests', () => {
6+
// Default local dev credentials from Supabase CLI
7+
const SUPABASE_URL = 'http://127.0.0.1:54321'
8+
const ANON_KEY =
9+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0'
10+
11+
const supabase = createClient(SUPABASE_URL, ANON_KEY, {
12+
realtime: { heartbeatIntervalMs: 500 },
13+
})
14+
15+
test('should connect to Supabase instance', async () => {
16+
expect(supabase).toBeDefined()
17+
expect(supabase).toBeInstanceOf(SupabaseClient)
18+
})
19+
20+
describe('PostgREST', () => {
21+
test('should query data from public schema', async () => {
22+
const { data, error } = await supabase.from('todos').select('*').limit(5)
23+
24+
// The default schema includes a 'todos' table, but it might be empty
25+
expect(error).toBeNull()
26+
expect(Array.isArray(data)).toBe(true)
27+
})
28+
29+
// Test creating and deleting data
30+
test('should create and delete a todo', async () => {
31+
// Create a new todo
32+
const { data: createdTodo, error: createError } = await supabase
33+
.from('todos')
34+
.insert({ task: 'Integration Test Todo', is_complete: false })
35+
.select()
36+
.single()
37+
38+
expect(createError).toBeNull()
39+
expect(createdTodo).toBeDefined()
40+
expect(createdTodo!.task).toBe('Integration Test Todo')
41+
expect(createdTodo!.is_complete).toBe(false)
42+
43+
// Delete the created todo
44+
const { error: deleteError } = await supabase.from('todos').delete().eq('id', createdTodo!.id)
45+
46+
expect(deleteError).toBeNull()
47+
48+
// Verify the todo was deleted
49+
const { data: fetchedTodo, error: fetchError } = await supabase
50+
.from('todos')
51+
.select('*')
52+
.eq('id', createdTodo!.id)
53+
.single()
54+
55+
expect(fetchError).not.toBeNull()
56+
expect(fetchedTodo).toBeNull()
57+
})
58+
})
59+
60+
describe('Authentication', () => {
61+
afterAll(async () => {
62+
// Clean up by signing out the user
63+
await supabase.auth.signOut()
64+
})
65+
test('should sign up a user', async () => {
66+
const email = `test-${Date.now()}@example.com`
67+
const password = 'password123'
68+
69+
const { data, error } = await supabase.auth.signUp({
70+
email,
71+
password,
72+
})
73+
74+
expect(error).toBeNull()
75+
expect(data.user).toBeDefined()
76+
expect(data.user!.email).toBe(email)
77+
})
78+
})
79+
80+
describe('Realtime', () => {
81+
const channelName = `channel-${crypto.randomUUID()}`
82+
let channel: RealtimeChannel
83+
let email: string
84+
let password: string
85+
86+
beforeEach(async () => {
87+
await supabase.auth.signOut()
88+
email = `test-${Date.now()}@example.com`
89+
password = 'password123'
90+
await supabase.auth.signUp({ email, password })
91+
92+
const config = { broadcast: { self: true }, private: true }
93+
channel = supabase.channel(channelName, { config })
94+
95+
await supabase.realtime.setAuth()
96+
})
97+
98+
afterEach(async () => {
99+
await supabase.removeAllChannels()
100+
})
101+
102+
test('is able to connect and broadcast', async () => {
103+
const testMessage = { message: 'test' }
104+
let receivedMessage: any
105+
let subscribed = false
106+
let attempts = 0
107+
108+
channel
109+
.on('broadcast', { event: '*' }, (payload) => (receivedMessage = payload))
110+
.subscribe((status) => {
111+
if (status == 'SUBSCRIBED') subscribed = true
112+
})
113+
114+
// Wait for subscription
115+
while (!subscribed) {
116+
if (attempts > 50) throw new Error('Timeout waiting for subscription')
117+
await new Promise((resolve) => setTimeout(resolve, 100))
118+
attempts++
119+
}
120+
121+
attempts = 0
122+
123+
channel.send({ type: 'broadcast', event: 'test-event', payload: testMessage })
124+
125+
// Wait on message
126+
while (!receivedMessage) {
127+
if (attempts > 50) throw new Error('Timeout waiting for message')
128+
await new Promise((resolve) => setTimeout(resolve, 100))
129+
attempts++
130+
}
131+
expect(receivedMessage).toBeDefined()
132+
expect(supabase.realtime.getChannels().length).toBe(1)
133+
}, 10000)
134+
})
135+
})

0 commit comments

Comments
 (0)
Please sign in to comment.