Repository: skeletonlabs/floating-ui-svelte (ts)

Issue:
Chore: Port missing `useRole` tests from React.
When #7 was closed it didn't have *all* the tests ported quite yet, this still needs to be done.

Pull request:
Chore: Add missing `useRole` tests
## Linked Issue

Closes #53

## Description

Adds missing tests for `useRole` which was long overdue

## Changesets

We use [Changesets](https://github.com/changesets/changesets) to automatically create our changelog per each release. Any changes or additions to the Library assets in `/lib` must be documented with a new Changeset. This can be done as follows:

1. Navigate to the root of the project on your feature branch.
2. Run `pnpm changeset` to trigger the Changeset CLI.
3. Follow the instructions when prompted.
    - Changesets should be either `minor` or `patch`. Never `major`.
    - Prefix your Changeset description using: `feature:`, `chore:` or `bugfix:`.
4. Changeset `.md` files are added to the `/.changeset` directory.
5. Commit and push the the new changeset file.

## Checklist

Please read and apply all [contribution requirements](https://github.com/skeletonlabs/floating-ui-svelte/blob/chore/main/CONTRIBUTING.md).

- [x] PR targets the `dev` branch (NEVER `master`)
- [x] All website documentation is current with your changes
- [x] Ensure Prettier formatting is current - run `pnpm format`
- [x] Ensure ESLint linting is current - run `pnpm lint`
- [x] All test cases are passing - run `pnpm test`
- [x] Includes a changeset (if relevant; see above)


Patch:
diff --git a/src/lib/hooks/useRole/index.svelte.ts b/src/lib/hooks/useRole/index.svelte.ts
index 6799904..582d80e 100644
--- a/src/lib/hooks/useRole/index.svelte.ts
+++ b/src/lib/hooks/useRole/index.svelte.ts
@@ -42,11 +42,24 @@ function useRole(context: FloatingContext, options: UseRoleOptions = {}): Elemen
 
 	const isNested = parentId != null;
 
+	const floatingProps = $derived({
+		id: floatingId,
+		...(ariaRole && { role: ariaRole }),
+	});
+
 	return {
+		// @ts-expect-error - variable prop is not specific enough
 		get reference() {
 			if (!enabled) {
 				return {};
 			}
+			if (ariaRole === 'tooltip' || role === 'label') {
+				return {
+					[`aria-${role === 'label' ? 'labelledby' : 'describedby'}` as const]: open
+						? floatingId
+						: undefined,
+				};
+			}
 			return {
 				'aria-expanded': open ? 'true' : 'false',
 				'aria-haspopup': ariaRole === 'alertdialog' ? 'dialog' : ariaRole,
@@ -62,9 +75,12 @@ function useRole(context: FloatingContext, options: UseRoleOptions = {}): Elemen
 			if (!enabled) {
 				return {};
 			}
+			if (ariaRole === 'tooltip' || role === 'label') {
+				return floatingProps;
+			}
 			return {
-				id: floatingId,
-				...(ariaRole && { role: ariaRole }),
+				...floatingProps,
+				...(ariaRole === 'menu' && { 'aria-labelledby': referenceId }),
 			};
 		},
 		get item() {


Test patch:
diff --git a/src/lib/hooks/useRole/App.test.svelte b/src/lib/hooks/useRole/App.test.svelte
index 9e0d075..a79d9d3 100644
--- a/src/lib/hooks/useRole/App.test.svelte
+++ b/src/lib/hooks/useRole/App.test.svelte
@@ -34,6 +34,11 @@
 		style={floating.floatingStyles}
 		{...interactions.getFloatingProps()}
 	>
-		Floating
+		{#each [1, 2, 3] as i}
+			<div
+				data-testid="item-{i}"
+				{...interactions.getItemProps({ active: i === 2, selected: i === 2 })}
+			></div>
+		{/each}
 	</div>
 {/if}
diff --git a/src/lib/hooks/useRole/index.test.svelte.ts b/src/lib/hooks/useRole/index.test.svelte.ts
index 4afec50..1fa2219 100644
--- a/src/lib/hooks/useRole/index.test.svelte.ts
+++ b/src/lib/hooks/useRole/index.test.svelte.ts
@@ -26,24 +26,192 @@ describe('useRole', () => {
 
 	describe('role', () => {
 		describe('dialog', () => {
+			it('applies the `dialog` role to the floating element', () => {
+				render(App, { role: 'dialog', open: true });
+
+				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'dialog');
+			});
+
 			it('applies the `aria-haspopup` attribute to the reference element', () => {
 				render(App, { role: 'dialog' });
 
 				expect(screen.getByTestId('reference')).toHaveAttribute('aria-haspopup', 'dialog');
 			});
 
-			it('applies the `dialog` role to the floating element', () => {
-				render(App, { role: 'dialog', open: true });
+			it('applies the `aria-expanded` attribute to the reference element based on `open` state', async () => {
+				const { rerender } = render(App, { role: 'dialog', open: false });
 
-				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'dialog');
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
 			});
 
-			it('applies the `aria-expanded` attribute to the reference element based on `open` state', async () => {
+			it('applies the `aria-controls` attribute with the correct id to the reference element based on the `open` state', async () => {
 				const { rerender } = render(App, { role: 'dialog', open: false });
 
+				expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-controls');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute(
+					'aria-controls',
+					screen.getByTestId('floating').id,
+				);
+			});
+		});
+
+		describe('label', () => {
+			it('applies the `aria-labelledby` attribute to the reference element', () => {
+				render(App, { role: 'label', open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-labelledby');
+			});
+		});
+
+		describe('tooltip', () => {
+			it('applies the `tooltip` role to the floating element', () => {
+				render(App, { role: 'tooltip', open: true });
+
+				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'tooltip');
+			});
+
+			it('applies the `aria-describedby` attribute to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'tooltip', open: false });
+
+				expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-describedby');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-describedby');
+			});
+		});
+
+		describe('menu', () => {
+			it('applies the `menu` role to the floating element', () => {
+				render(App, { role: 'menu', open: true });
+
+				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'menu');
+			});
+
+			it('applies the `aira-haspopup` attribute to the reference element', () => {
+				render(App, { role: 'menu' });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-haspopup', 'menu');
+			});
+
+			it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'menu', open: false });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
+			});
+
+			it('applies the `aria-controls` attribute with the correct id to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'menu', open: false });
+
+				expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-controls');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute(
+					'aria-controls',
+					screen.getByTestId('floating').id,
+				);
+			});
+		});
+
+		describe('listbox', () => {
+			it('applies the `listbox` role to the floating element', () => {
+				render(App, { role: 'listbox', open: true });
+
+				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'listbox');
+			});
+
+			it('applies the `aria-haspopup` attribute to the reference element', () => {
+				render(App, { role: 'listbox' });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-haspopup', 'listbox');
+			});
+
+			it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'listbox', open: false });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
+			});
+
+			it('applies the `aria-controls` attribute with the correct id to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'listbox', open: false });
+
+				expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-controls');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute(
+					'aria-controls',
+					screen.getByTestId('floating').id,
+				);
+			});
+		});
+
+		describe('select', () => {
+			it('applies the `listbox` role to the floating element', () => {
+				render(App, { role: 'select', open: true });
+
+				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'listbox');
+			});
+
+			it('applies the `aria-autocomplete` attribute to the reference element', () => {
+				render(App, { role: 'select' });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-autocomplete', 'none');
+			});
+
+			it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'select', open: false });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
+
+				await rerender({ open: true });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
+			});
+
+			it('applies the `aria-selected` attribute to the selected item within the floating element', async () => {
+				render(App, { role: 'select', open: true });
+
+				expect(screen.getByTestId('item-1')).toHaveAttribute('aria-selected', 'false');
+				expect(screen.getByTestId('item-2')).toHaveAttribute('aria-selected', 'true');
+			});
+		});
+
+		describe('combobox', () => {
+			it('applies the `listbox` role to the floating element', () => {
+				render(App, { role: 'combobox', open: true });
+
+				expect(screen.getByTestId('floating')).toHaveAttribute('role', 'listbox');
+			});
+
+			it('applies the `aria-autocomplete` attribute to the reference element', () => {
+				render(App, { role: 'combobox' });
+
+				expect(screen.getByTestId('reference')).toHaveAttribute('aria-autocomplete', 'list');
+			});
+
+			it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
+				const { rerender } = render(App, { role: 'combobox', open: false });
+
 				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
 
-				await rerender({ role: 'dialog', open: true });
+				await rerender({ open: true });
 
 				expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
 			});