Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add React hooks calls and integration tests for real-time updates (#88) #118

Merged
merged 10 commits into from
Jun 30, 2021
47 changes: 47 additions & 0 deletions cypress/integration/ws/mock-ws.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Mock WebSocket class for Graasp WS protocol
*/

// eslint-disable-next-line import/prefer-default-export
export class WebSocket {
constructor() {
this.CLOSED = 0;
this.OPEN = 1;

this.readyState = this.OPEN;

this.send = this.send.bind(this);
this.receive = this.receive.bind(this);
this.addEventListener = this.addEventListener.bind(this);
}

send(msg) {
const req = JSON.parse(msg);
// acknowledge request
if (req.action.includes('subscribe')) {
const res = {
data: JSON.stringify({
realm: 'notif',
type: 'response',
status: 'success',
request: req,
}),
};
this.onmessage(res);
}
}

receive(msg) {
const event = {
data: JSON.stringify(msg),
};
this.onmessage(event);
}

addEventListener(event, handler) {
this[`on${event}`] = handler;
if (event === 'open') {
handler();
}
}
}
131 changes: 131 additions & 0 deletions cypress/integration/ws/ws.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { buildItemPath, SHARED_ITEMS_PATH } from '../../../src/config/paths';
import { buildItemsTableRowId } from '../../../src/config/selectors';
import { SAMPLE_ITEMS } from '../../fixtures/items';
import { CURRENT_USER } from '../../fixtures/members';
import { WebSocket } from './mock-ws';

describe('Websocket interactions', () => {
let client;

beforeEach(() => {
client = new WebSocket();
});

// paramaterized before, to be called in each test or in beforeEach
const beforeWs = (visitRoute, sampleData) => {
cy.setUpApi(sampleData);
cy.visit(visitRoute, {
onBeforeLoad: (win) => {
cy.stub(win, 'WebSocket', () => client);
},
});
};

describe('sharedWith me items updates', () => {
it('displays sharedWith create update', () => {
beforeWs(SHARED_ITEMS_PATH, { items: [] });

const item = SAMPLE_ITEMS.items[0];
cy.wait('@getSharedItems').then(() => {
// send mock sharedItem create update
client.receive({
realm: 'notif',
type: 'update',
channel: CURRENT_USER.id,
body: {
entity: 'member',
kind: 'sharedWith',
op: 'create',
value: item,
codeofmochi marked this conversation as resolved.
Show resolved Hide resolved
},
});
});

// assert item is in list
cy.get(`#${buildItemsTableRowId(item.id)}`).should('exist');
});

it('displays sharedWith delete update', () => {
// create items that do not belong to current user
const items = SAMPLE_ITEMS.items.map((i) => ({
...i,
creator: 'someoneElse',
}));
const item = items[0];
beforeWs(SHARED_ITEMS_PATH, { items });

cy.get(`#${buildItemsTableRowId(item.id)}`).then(() => {
// send mock sharedItem delete update
client.receive({
realm: 'notif',
type: 'update',
channel: CURRENT_USER.id,
body: {
entity: 'member',
kind: 'sharedWith',
op: 'delete',
value: item,
},
});
});

// assert item is not in list anymore
cy.get(`#${buildItemsTableRowId(item.id)}`).should('not.exist');
});
});

describe('childItem updates', () => {
const { id } = SAMPLE_ITEMS.items[0];

beforeEach(() => {
beforeWs(buildItemPath(id), SAMPLE_ITEMS);

// should get children
cy.wait('@getChildren').then(({ response: { body } }) => {
// check item is created and displayed
for (const item of body) {
cy.get(`#${buildItemsTableRowId(item.id)}`).should('exist');
}
});
});

it('displays childItem create update', () => {
const item = { ...SAMPLE_ITEMS.items[0], id: 'child0' };
// send mock childItem create update
client.receive({
realm: 'notif',
type: 'update',
channel: id,
body: {
entity: 'item',
kind: 'childItem',
op: 'create',
value: item,
},
});

// assert item is in list
cy.get(`#${buildItemsTableRowId(item.id)}`).should('exist');
});

it('displays childItem delete update', () => {
// this item MUST be a child of id above
const item = SAMPLE_ITEMS.items[2];
// send mock childItem delete update
client.receive({
realm: 'notif',
type: 'update',
channel: id,
body: {
entity: 'item',
kind: 'childItem',
op: 'delete',
value: item,
},
});

// assert item is not in list
cy.get(`#${buildItemsTableRowId(item.id)}`).should('not.exist');
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@graasp/query-client": "git://github.com/graasp/graasp-query-client.git",
"@graasp/query-client": "git://github.com/graasp/graasp-query-client.git#9/websockets",
"@graasp/ui": "git://github.com/graasp/graasp-ui.git#master",
"@material-ui/core": "4.11.2",
"@material-ui/icons": "4.11.2",
Expand Down
6 changes: 4 additions & 2 deletions src/components/SharedItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ import {
import ItemHeader from './item/header/ItemHeader';
import ErrorAlert from './common/ErrorAlert';
import Items from './main/Items';
import { hooks } from '../config/queryClient';
import { hooks, ws } from '../config/queryClient';
import Loader from './common/Loader';
import Main from './main/Main';

const SharedItems = () => {
const { t } = useTranslation();
const { data: sharedItems, isLoading, isError } = hooks.useSharedItems();
const { data: user, isUserLoading } = hooks.useCurrentMember();
ws.hooks.useSharedItemsUpdates(user?.get('id'));

if (isError) {
return <ErrorAlert id={SHARED_ITEMS_ERROR_ALERT_ID} />;
}

if (isLoading) {
if (isLoading || isUserLoading) {
return <Loader />;
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/main/ItemScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
AppItem,
} from '@graasp/ui';
import { MUTATION_KEYS } from '@graasp/query-client';
import { hooks, useMutation } from '../../config/queryClient';
import { hooks, useMutation, ws } from '../../config/queryClient';
import Items from './Items';
import {
buildFileItemId,
Expand Down Expand Up @@ -48,6 +48,7 @@ const ItemScreen = () => {

// display children
const { data: children, isLoading: isChildrenLoading } = useChildren(itemId);
ws.hooks.useChildrenUpdates(itemId);
const id = item?.get(ITEM_KEYS.ID);

const { data: content } = useFileContent(id, {
Expand Down
2 changes: 2 additions & 0 deletions src/config/queryClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
queryClient,
QueryClientProvider,
hooks,
ws,
useMutation,
ReactQueryDevtools,
API_ROUTES,
Expand All @@ -18,6 +19,7 @@ export {
queryClient,
QueryClientProvider,
hooks,
ws,
useMutation,
ReactQueryDevtools,
API_ROUTES,
Expand Down