Giter Club home page Giter Club logo

supawright's Introduction

Supawright

Supawright is a Playwright test harness for E2E testing with Supabase.

Supawright can create database tables and records for you, and will clean up after itself when the test exits. It will create records recursively based on foreign key constraints, and will automatically discover any related records that were not created by Supawright and delete them as well.

Installation

pnpm i -D supawright

Usage

Setup

Unfortunately, Supabase's generated TypeScript types generate an interface, whereas for type constraints, we need a type. So, first change the following line in your generated Supabase types (typically database.ts):

Note: this has now been changed, but will still apply to old Supabase versions.

- export interface Database {
+ export type Database = {

I recommend setting up a make target (or whichever build tool you use) to automatically make this change for you, e.g.

types:
    pnpm supabase gen types typescript --local | \
    sed 's/export interface Database {/export type Database = {/' \
    > src/types/database.ts

Then, create a test file, e.g. can-login.test.ts, and create a test function with the withSupawright function:

import { withSupawright } from 'supawright'
import type { Database } from './database'

const test = withSupawright<
  Database,
  'public' | 'other' // Note 1
>(['public', 'other'])

1: Unfortunately, I haven't found a nice way of infering the schema names from the first argument, so you'll have to specify the schemas you'd like Supawright to use in two places.

Tests

Assuming you have a test function as above, you can now write tests and use the supawright fixture to recursively create database tables. Consider the following table structure:

create table public."user" (
    id uuid primary key default uuid_generate_v4(),
    email text not null unique,
    password text not null
);

create table public.session (
    id uuid primary key default uuid_generate_v4(),
    user_id uuid not null references public."user"(id),
    token text,
    created_at timestamp with time zone not null default now()
);

If you use Supawright to create a session, it will automatically create a user for you, and you can access the user's id in the session's user_id column. Supawright will also automatically generate fake data for any columns that are not nullable and do not have a default value.

test('can login', async ({ supawright }) => {
  const session = await supawright.create('public', 'session')
  expect(session.user_id).toBeDefined()
})

You can optionally pass a data object as the second argument to the create function to override the fake data that is generated. If you pass in data for a foreign key column, Supawright will not create a record for that table.

If your table is in the public schema, you can omit the schema name:

test('can login', async ({ supawright }) => {
  const user = await supawright.create('user', {
    email: '[email protected]'
  })
  const session = await supawright.create('session', {
    user_id: user.id
  })
  // Supawright will not create a user record, since we've passed in
  // a user_id.
  const { data: users } = await supawright.supabase().from('user').select()
  expect(users.length).toBe(1)
})

When the test exits, Supawright will automatically clean up all the records it has created, and will inspect foreign key constraints to delete records in the correct order.

It will also discover any additional records in the database that were not created by Supawright, and will delete them as well, provided they have a foreign key relationship with a record that was created by Supawright.

This runs recursively. Consider the following example:

test('can login', async ({ supawright }) => {
  const user = await supawright.create('user')

  // Since we're using the standard Supabase client here, Supawright
  // is unaware of the records we're creating.
  await supawright
    .supabase()
    .from('session')
    .insert([{ user_id: user.id }, { user_id: user.id }])

  // However, Supawright will discover these records and delete
  // them when the test exits.
})

Note: the .supabase() method of the Supawright object takes an optional schema name to create a Supabase client in the chosen schema.

Overrides

If you have custom functions you wish to use to generate fake data or create records, you can pass optional config as the second argument to the withSupawright function.

The generators object is a record of Postgres types to functions that return a value of that type. Supawright will use these functions to generate fake data for any columns that are not nullable and do not have a default value.

If you're using user defined types, specify the USER-DEFINED type name in the generators object. This will be used for enums, for example.

The overrides object is a record of schema names to a record of table names to functions that return a record of column names to values. Supawright will use these functions to create records in the database. These return an array of Fixtures which Supawright will use to record the records it has created.

This is useful if you use a database trigger to populate certain tables and need to run custom code to activate the trigger.

const test = withSupawright<
    Database,
    'public' | 'other',
>(
    ['public', 'other'],
    {
        generators: {
            smallint: () => 123,
            text: (table: string, column: string) => `${table}.${column}`,
        },
        overrides: {
            public: {
                user: async ({ supawright, data, supabase, generators }) => {
                    const { data: user } = await supabase
                        .from('user')
                        .insert(...)
                        .select()
                        .single()
                    ...
                    return [{
                        schema: 'public',
                        table: 'user',
                        data: user,
                    }]
                }
            }
        }
    }
)

If your generator returns null or undefined, Supawright will fall back to using the built-in generators. In the case of enums, Supawright will pick a random valid enum value.

Connection details

By default, Supawright will look for the SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY environment variables to connect to your Supabase instance. You can override these using the supabase key in the config object.

Supawright also needs access to a Supabase database for schema inspection, and will use the default Supabase localhost database. If you'd like to override this, provide a database key in the config object.

const test = withSupawright<Database, 'public' | 'other'>(['public', 'other'], {
  supabase: {
    supabaseUrl: 'my-supabase-url.com',
    serviceRoleKey: 'my-service-role-key'
  },
  database: {
    host: 'localhost',
    port: 54322,
    user: 'me',
    password: 'password',
    database: 'my-database'
  }
})

Support

Are you a company using Supawright? Get in touch if you're interested in supporting the package, or if you'd like premium support.

TODO

  • Automatically infer allowed enum values from database
  • Automatically infer custom composite types from database
  • Fix up my janky typings
  • Come up with a way of using the Database type without having to modify the generated Supabase types
    • This may involve convincing Supabase to change up their generated types

supawright's People

Contributors

isaacharrisholt avatar github-actions[bot] avatar

Stargazers

Miguel ☕ avatar Alexander Mikuta avatar Khuyen Nguyen avatar Daniel Wei avatar Scott de Jong avatar James avatar Giuseppe De Palma avatar  avatar Chew Chit Siang avatar Thomas Brasington avatar Jits avatar PlatformKit avatar Chris Webb avatar Rob avatar Fred avatar Faye  avatar Eduardo del Palacio Lirola avatar Kristoffer Eriksson avatar Nemanja avatar Madi avatar fadri1 avatar Henry Fawcett avatar Yihwan Kim avatar Dawit Mekonnen avatar Rohith reddy avatar  avatar Leon Guillaume avatar Evgenii Khramkov avatar  avatar Timm Stokke avatar  avatar Kajetan Szymczak avatar Tomáš Hübelbauer avatar Bohdan avatar John Rees avatar Jack Westmore avatar

Watchers

 avatar

Forkers

0xbigboss yykcool

supawright's Issues

Handle creating auth.users

Hey Supawright is working great, but thought I'd bring up a workaround that I'm facing in the hopes that there is an easy way to implement this with supawright. A lot of our tables have auth.users as a foreign key constraint so it forces us to create one before calling supawright.create. See below as an example of what I'm talking about or here in the repo.

import { mergeTests } from '@playwright/test'
import { test as sendAccountTest, expect } from '@my/playwright/fixtures/send-accounts'
import { test as supawrightTest } from '@my/playwright/fixtures/supawright'
import { debug, Debugger } from 'debug'
import { supabaseAdmin } from 'app/utils/supabase/admin'
import { countries } from 'app/utils/country'

const randomCountry = () =>
  countries[Math.floor(Math.random() * countries.length)] as (typeof countries)[number]

const test = mergeTests(sendAccountTest, supawrightTest)

let log: Debugger
let otherUserId: string

test.beforeEach(async ({ page }) => {
  log = debug(`test:profile:${test.info().parallelIndex}`)
  const randomNumber = Math.floor(Math.random() * 1e9)
  const country = randomCountry()
  const { data, error } = await supabaseAdmin.auth.signUp({
    phone: `+${country.dialCode}${randomNumber}`,
    password: 'changeme',
  })
  if (error) {
    log('error creating user', error)
    throw error
  }
  if (!data?.user) {
    throw new Error('user not created')
  }
  if (!data?.session) {
    throw new Error('session not created')
  }
  log('created user', data)
  otherUserId = data.user.id
})

test.afterEach(async () => {
  const { parallelIndex } = test.info()
  await supabaseAdmin.auth.admin.deleteUser(otherUserId).then(({ error }) => {
    if (error) {
      log('error deleting user', `id=${parallelIndex}`, `user=${otherUserId}`, error)
      throw error
    }
  })
})

test('should work', async ({ page, supawright }) => {
  expect(otherUserId).toBeDefined()
  const result = await supawright.create('tags', {
    name: 'tag1',
    status: 'confirmed',
    user_id: otherUserId,
  })
  log('created tag1', result)
  expect(result).toBeDefined()
  await page.goto('/profile/tag1')
  const title = await page.title()
  expect(title).toBe('Send | Profile')
})

user_id in table

Hi,
In my user_details table, the id is one to one relation with auth.users.id.

when i try to run the test

test('can create table with no dependencies', async ({ supawright }) => {
    await supawright.create('user_details')
  })

i get Error: Error inserting data into user_details: insert or update on table "user_details" violates foreign key constraint "user_details_id_fkey"

How do i fix this?

Use `udt_name` for `USER-DEFINED` data types as the generator key

Our database uses quite a large number of USER-DEFINED data types in postgres such as enum and citext. This forces all of them to be handled by one USER-DEFINED generator function.

Consider using the udt_name when building the tree and using that as the key for the generator function. Reference..)

https://github.com/isaacharrisholt/supawright/blob/main/src/tree.ts#L42

That will also allow you to join on the possible enum values.

select table_name, column_name, data_type, udt_name
from information_schema.columns
where table_schema in (${schemasString});
-- pull all enum values in a schema
select n.nspname as enum_schema,
       t.typname as enum_name,
       e.enumlabel as enum_value
from pg_type t
         join pg_enum e on t.oid = e.enumtypid
         join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
where n.nspname in (${schemasString});

Receiving 'AggregateError:'

Hey,
pretty new to testing but I don't find what is wrong with my code or if it could be a bug?

This is the piece of code that is throwing an error:

import { withSupawright } from "supawright";
import type { Database } from "~/types/supabase";

const test = withSupawright<Database, "public">(["public"]);

// test.use({ storageState: { cookies: [], origins: [] } });

test("can create transaction", async ({ supawright }) => {
	console.log("context", supawright);
});

The only text I get is this:

  1) [firefox] › e2e/transactions.spec.ts:9:1 › can create transaction ─────────────────────────────

    AggregateError:

  1 failed
    [firefox] › e2e/transactions.spec.ts:9:1 › can create transaction ──────────────────────────────

I have no idea what I'm doing wrong.
Any help would be appreciated thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.