# Invite a member and join

> An owner invites a member by email, and the member joins and sees the project.

Source: https://rehearsal.dev/docs/examples/invite-a-member

This example uses Parcel, a demo store at `parcel.example.com`. Parcel is a demo project, not a customer.

Two people take part. The owner invites a member by email. The member accepts from the email and sees the Parcel project.

## Before you start

Each person needs a [test account](https://rehearsal.dev/docs/reagent/test-accounts.md). Add both under **Re:agent uses**, in **Test accounts**.

| Name | Kind | Address |
| --- | --- | --- |
| owner | **Password** | `owner@parcel.example.com`, with its password |
| member | **With an email code or link** | Rehearsal makes one, such as `parcel-member@inbox.rehearsal.dev` |

The member needs an account that signs in with an email code or link, because the invitation must reach its [test inbox](https://rehearsal.dev/docs/reagent/test-inbox.md). An account with a password gets an address only when its code after the password is sent by email.

## Ask Re:agent

Open **Re:agent** and type: "Check that an invited member can join Parcel and see the project. The owner sends the invitation."

## What Re:agent does

Re:agent opens a browser for each person, as [flows with more than one person](https://rehearsal.dev/docs/reagent/more-than-one-person.md) describes. Each line below is a row in the chat, in the app's own words. What follows a dot is the detail the chat shows beside the row.

- Checked your projects
- Listed 2 test accounts

In the owner's browser:

- Opened a browser on **parcel.example.com** as owner
- Opened **parcel.example.com/sign-in**
- Filled **Email** · `owner@parcel.example.com`
- Filled **Password**
- Clicked **Sign in**
- Clicked **Members**
- Clicked **Invite a member**
- Filled **Email** · `parcel-member@inbox.rehearsal.dev`
- Chose an option in **Role** · Member
- Clicked **Send invitation**
- Read the page · Invitation sent

In the member's browser:

- Opened a browser on **parcel.example.com** as member
- Read the test inbox · 1 message
- Opened **parcel.example.com/invite**
- Filled **Name** · Mira Okafor
- Clicked **Accept invitation**
- Read the page · Parcel

The last row is **Saved the flow**.

Re:agent never sees the owner's password. It types `{{password}}`, and Rehearsal fills in the real one. The link from the email is written down as `{{link}}`.

Then a card in the chat follows the new test. It moves through **Flow saved**, **Test written**, **First run passed** and **Verified in a fresh browser**.

## The test

Rehearsal writes a Playwright test from the saved flow. It looks like this, with a browser for each person.

```ts
import { expect, test } from '@playwright/test';
import { participant } from '@rehearsal/codegen/runtime';

test('Invited member can join and see the project', async ({ browser }) => {
  const owner = await participant(browser, 'owner');
  const member = await participant(browser, 'member');
  try {
    await test.step('Sign in as the owner', async () => {
      await owner.page.goto('/sign-in');
      await owner.page.getByLabel('Email').fill('owner@parcel.example.com');
      await owner.page.getByLabel('Password').fill(owner.password());
      await owner.page.getByRole('button', { name: 'Sign in' }).click();
      await owner.page.waitForURL('**/projects');
    });
    await test.step('Invite the member', async () => {
      await owner.page.getByRole('link', { name: 'Members' }).click();
      await owner.page.getByRole('button', { name: 'Invite a member' }).click();
      await owner.page.getByLabel('Email').fill('parcel-member@inbox.rehearsal.dev');
      await owner.page.getByLabel('Role').selectOption('Member');
      await owner.page.getByRole('button', { name: 'Send invitation' }).click();
    });
    await test.step('Check the invitation is listed', async () => {
      await expect(owner.page.getByText('parcel-member@inbox.rehearsal.dev'))
        .toBeVisible();
    });
    await test.step('Open the invitation as the member', async () => {
      const email = await member.latestEmail({ subject: 'invited' });
      await member.page.goto(email.links[0]);
    });
    await test.step('Accept the invitation', async () => {
      await member.page.getByLabel('Name').fill('Mira Okafor');
      await member.page.getByRole('button', { name: 'Accept invitation' }).click();
      await member.page.waitForURL('**/projects');
    });
    await test.step('Check the member sees the Parcel project', async () => {
      await expect(member.page.getByRole('link', { name: 'Parcel' })).toBeVisible();
    });
  }
  finally {
    await owner.close();
    await member.close();
  }
});
```

`owner.password()` reads the password when the test runs. `member.latestEmail()` waits for the invitation in the member's test inbox. Neither value is in the file.

> [!NOTE]
> The member's address stays the same on every run. If your app rejects a second invitation to it, ask Re:agent to end the flow by removing the member.

## Run it

Press **Run now** on the card in the chat, or **Run** on the test's row in **Tests**. From the terminal:

```sh
rehearsal runs start --test <test-id>
rehearsal runs wait <run-id>
```

The test uses two browsers and still [counts as one test run](https://rehearsal.dev/docs/billing/plans-and-runs.md).

## Next steps

- Read how [Re:agent creates a test](https://rehearsal.dev/docs/reagent/create-a-test.md) in the chat.
- [Run tests from the terminal](https://rehearsal.dev/docs/cli/run-tests.md) and read their results.
- Try a flow with one person: [Sign up with an email code](https://rehearsal.dev/docs/examples/sign-up-with-an-email-code.md).
