Merge branch 'test/add-cypress-on-event-creation' into 'master'

Add e2e seed and test event creation

See merge request framasoft/mobilizon!254
This commit is contained in:
Thomas Citharel 2019-10-12 19:47:19 +02:00
commit 2577a2a27b
16 changed files with 237 additions and 30 deletions

View File

@ -91,6 +91,7 @@ cypress:
- cd ../ - cd ../
- MIX_ENV=e2e mix ecto.create - MIX_ENV=e2e mix ecto.create
- MIX_ENV=e2e mix ecto.migrate - MIX_ENV=e2e mix ecto.migrate
- MIX_ENV=e2e mix run priv/repo/e2e.seed.exs
- MIX_ENV=e2e mix phx.server & - MIX_ENV=e2e mix phx.server &
- cd js - cd js
- npx wait-on http://localhost:4000 - npx wait-on http://localhost:4000

View File

@ -7,7 +7,8 @@ import Config
# General application configuration # General application configuration
config :mobilizon, config :mobilizon,
ecto_repos: [Mobilizon.Storage.Repo] ecto_repos: [Mobilizon.Storage.Repo],
env: Mix.env()
config :mobilizon, :instance, config :mobilizon, :instance,
name: System.get_env("MOBILIZON_INSTANCE_NAME") || "My Mobilizon Instance", name: System.get_env("MOBILIZON_INSTANCE_NAME") || "My Mobilizon Instance",

View File

@ -20,7 +20,7 @@
<template> <template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span> <span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span>
<span v-else-if="isSameDay()"> <span v-else-if="isSameDay()">
{{ $t('The {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }} {{ $t('On {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }}
</span> </span>
<span v-else-if="endsOn"> <span v-else-if="endsOn">
{{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}', {{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}',

View File

@ -137,8 +137,8 @@ export default class NavBar extends Vue {
} }
} }
async handleErrors(errors: GraphQLError) { async handleErrors(errors: GraphQLError[]) {
if (errors[0].message === 'You need to be logged-in to view your list of identities') { if (errors.length > 0 && errors[0].message === 'You need to be logged-in to view your list of identities') {
await this.logout(); await this.logout();
} }
} }

View File

@ -145,6 +145,7 @@
"No results for \"{queryText}\"": "No results for \"{queryText}\"", "No results for \"{queryText}\"": "No results for \"{queryText}\"",
"Number of places": "Number of places", "Number of places": "Number of places",
"Old password": "Old password", "Old password": "Old password",
"On {date} from {startTime} to {endTime}": "On {date} from {startTime} to {endTime}",
"One person is going": "No one is going | One person is going | {approved} persons are going", "One person is going": "No one is going | One person is going | {approved} persons are going",
"Only accessible through link and search (private)": "Only accessible through link and search (private)", "Only accessible through link and search (private)": "Only accessible through link and search (private)",
"Opened reports": "Opened reports", "Opened reports": "Opened reports",
@ -218,13 +219,13 @@
"The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.", "The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.",
"The password was successfully changed": "The password was successfully changed", "The password was successfully changed": "The password was successfully changed",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.",
"The {date} from {startTime} to {endTime}": "The {date} from {startTime} to {endTime}",
"These events may interest you": "These events may interest you", "These events may interest you": "These events may interest you",
"This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.", "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.",
"This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.", "This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.",
"This is a demonstration site to test the beta version of Mobilizon.": "This is a demonstration site to test the beta version of Mobilizon.", "This is a demonstration site to test the beta version of Mobilizon.": "This is a demonstration site to test the beta version of Mobilizon.",
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.", "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.",
"Title": "Title", "Title": "Title",
"To achieve your registration, please create a first identity profile.": "To achieve your registration, please create a first identity profile.",
"To change the world, change the software": "To change the world, change the software", "To change the world, change the software": "To change the world, change the software",
"To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"", "To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"",
"Transfer to {outsideDomain}": "Transfer to {outsideDomain}", "Transfer to {outsideDomain}": "Transfer to {outsideDomain}",

View File

@ -170,6 +170,7 @@
"No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »", "No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
"Number of places": "Nombre de places", "Number of places": "Nombre de places",
"Old password": "Ancien mot de passe", "Old password": "Ancien mot de passe",
"On {date} from {startTime} to {endTime}": "On {date} de {startTime} à {endTime}",
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont", "One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)", "Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
"Opened reports": "Signalements ouverts", "Opened reports": "Signalements ouverts",
@ -250,7 +251,6 @@
"The password was successfully changed": "Le mot de passe a été changé avec succès", "The password was successfully changed": "Le mot de passe a été changé avec succès",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.",
"The {date} at {time}": "Le {date} à {time}", "The {date} at {time}": "Le {date} à {time}",
"The {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
"There are {participants} participants.": "Il n'y a qu'un⋅e participant⋅e. | Il y a {participants} participants.", "There are {participants} participants.": "Il n'y a qu'un⋅e participant⋅e. | Il y a {participants} participants.",
"These events may interest you": "Ces événements peuvent vous intéresser", "These events may interest you": "Ces événements peuvent vous intéresser",
"This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "Cette installation (appelée “instance“) peut facilement {interconnect}, grâce à {protocol}.", "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "Cette installation (appelée “instance“) peut facilement {interconnect}, grâce à {protocol}.",
@ -258,6 +258,7 @@
"This is a demonstration site to test the beta version of Mobilizon.": "Ceci est un site de démonstration permettant de tester la version bêta de Mobilizon.", "This is a demonstration site to test the beta version of Mobilizon.": "Ceci est un site de démonstration permettant de tester la version bêta de Mobilizon.",
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "Cela supprimera / anonymisera tout le contenu (événements, commentaires, messages, participations…) créés avec cette identité.", "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "Cela supprimera / anonymisera tout le contenu (événements, commentaires, messages, participations…) créés avec cette identité.",
"Title": "Titre", "Title": "Titre",
"To achieve your registration, please create a first identity profile.": "Pour finir votre inscription, veuillez créer un premier profil.",
"To change the world, change the software": "Changer de logiciel pour changer le monde", "To change the world, change the software": "Changer de logiciel pour changer le monde",
"To confirm, type your event title \"{eventTitle}\"": "Pour confirmer, entrez le titre de l'événement « {eventTitle} »", "To confirm, type your event title \"{eventTitle}\"": "Pour confirmer, entrez le titre de l'événement « {eventTitle} »",
"To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de lidentité « {preferredUsername} »", "To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de lidentité « {preferredUsername} »",

View File

@ -37,6 +37,8 @@ export function deleteUserData() {
} }
} }
export class NoIdentitiesException extends Error {}
/** /**
* We fetch from localStorage the latest actor ID used, * We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache * then fetch the current identities to set in cache
@ -50,7 +52,10 @@ export async function initializeCurrentActor(apollo: ApolloClient<any>) {
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}); });
const identities = result.data.identities; const identities = result.data.identities;
if (identities.length < 1) return; if (identities.length < 1) {
console.warn('Logged user has no identities!');
throw new NoIdentitiesException;
}
const activeIdentity = identities.find(identity => identity.id === actorId) || identities[0] as IPerson; const activeIdentity = identities.find(identity => identity.id === actorId) || identities[0] as IPerson;
if (activeIdentity) { if (activeIdentity) {

View File

@ -5,7 +5,10 @@
<h1 class="title"> <h1 class="title">
{{ $t('Register an account on Mobilizon!') }} {{ $t('Register an account on Mobilizon!') }}
</h1> </h1>
<form v-if="!validationSent"> <b-message v-if="userAlreadyActivated">
{{ $t('To achieve your registration, please create a first identity profile.')}}
</b-message>
<form v-if="!validationSent" @submit.prevent="submit">
<b-field <b-field
:label="$t('Username')" :label="$t('Username')"
:type="errors.preferred_username ? 'is-danger' : null" :type="errors.preferred_username ? 'is-danger' : null"
@ -33,7 +36,7 @@
</b-field> </b-field>
<p class="control has-text-centered"> <p class="control has-text-centered">
<b-button type="is-primary" size="is-large" @click="submit()"> <b-button type="is-primary" size="is-large" native-type="submit">
{{ $t('Create my profile') }} {{ $t('Create my profile') }}
</b-button> </b-button>
</p> </p>
@ -117,8 +120,8 @@ export default class Register extends Vue {
acc[error.details] = error.message; acc[error.details] = error.message;
return acc; return acc;
}, {}); }, {});
console.error(error); console.error('Error while registering person', error);
console.error(this.errors); console.error('Errors while registering person', this.errors);
} }
} }
} }

View File

@ -62,7 +62,7 @@
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { LOGIN } from '@/graphql/auth'; import { LOGIN } from '@/graphql/auth';
import { validateEmailField, validateRequiredField } from '@/utils/validators'; import { validateEmailField, validateRequiredField } from '@/utils/validators';
import { initializeCurrentActor, saveUserData } from '@/utils/auth'; import { initializeCurrentActor, NoIdentitiesException, saveUserData } from '@/utils/auth';
import { ILogin } from '@/types/login.model'; import { ILogin } from '@/types/login.model';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'; import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogin } from '@/vue-apollo'; import { onLogin } from '@/vue-apollo';
@ -153,7 +153,16 @@ export default class Login extends Vue {
role: data.login.user.role, role: data.login.user.role,
}, },
}); });
await initializeCurrentActor(this.$apollo.provider.defaultClient); try {
await initializeCurrentActor(this.$apollo.provider.defaultClient);
} catch (e) {
if (e instanceof NoIdentitiesException) {
return await this.$router.push({
name: RouteName.REGISTER_PROFILE,
params: { email: this.currentUser.email, userAlreadyActivated: 'true' },
});
}
}
onLogin(this.$apollo); onLogin(this.$apollo);

View File

@ -1,14 +1,6 @@
// https://docs.cypress.io/api/introduction/api.html // https://docs.cypress.io/api/introduction/api.html
import { onBeforeLoad } from './browser-language'; import { onBeforeLoad } from './browser-language';
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
describe('Homepage', () => { describe('Homepage', () => {
it('Checks the footer', () => { it('Checks the footer', () => {
cy.visit('/', { onBeforeLoad }); cy.visit('/', { onBeforeLoad });

View File

@ -0,0 +1,44 @@
import { onBeforeLoad } from './browser-language';
beforeEach(() => {
cy.clearLocalStorage();
cy.checkoutSession();
});
afterEach(() => {
cy.dropSession();
});
describe('Events', () => {
it('Shows my current events', () => {
const EVENT = { title: 'My first event'};
cy.loginUser();
cy.visit('/events/me', { onBeforeLoad });
cy.contains('.message.is-danger', 'No events found');
cy.contains('.navbar-item', 'Create').click();
cy.url().should('include', 'create');
cy.get('.field').first().find('input').type(EVENT.title);
cy.get('.field').eq(1).find('input').type('my tag, holo{enter}');
cy.get('.field').eq(2).find('.datepicker .dropdown-trigger').click();
cy.get('.field').eq(3).find('.pagination-list .control').first().find('.select select').select('September');
cy.get('.field').eq(3).find('.pagination-list .control').last().find('.select select').select('2021');
cy.wait(1000);
cy.get('.field').eq(3).contains('.datepicker-cell', '15').click();
cy.contains('.button.is-primary', 'Create my event').click();
cy.url().should('include', '/events/');
cy.contains('.title', EVENT.title);
cy.contains('.title-and-informations span small', 'You\'re the only one going to this event');
cy.contains('.date-and-privacy', 'On Wednesday, September 15, 2021 from');
cy.contains('.visibility .tag', 'Public event');
cy.contains('.navbar-item', 'My events').click();
cy.contains('.title', EVENT.title);
cy.contains('.content.column', 'You\'re organizing this event');
cy.contains('.title-wrapper .date-component .datetime-container .month', 'Sep');
cy.contains('.title-wrapper .date-component .datetime-container .day', '15');
});
});

View File

@ -1,11 +1,7 @@
import { onBeforeLoad } from './browser-language'; import { onBeforeLoad } from './browser-language';
beforeEach(() => { beforeEach(() => {
cy.restoreLocalStorage(); cy.clearLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
}); });
describe('Login', () => { describe('Login', () => {
@ -42,4 +38,46 @@ describe('Login', () => {
cy.contains('.message.is-danger', 'User with email not found'); cy.contains('.message.is-danger', 'User with email not found');
}); });
it('Tries to login with valid credentials', () => {
cy.visit('/login', { onBeforeLoad });
cy.get('input[type=email]').type('user@email.com');
cy.get('input[type=password]').type('some password');
cy.get('form').submit();
cy.contains('.navbar-link', 'test_user');
cy.contains('article.message.is-info', 'Welcome back I\'m a test user');
cy.contains('.navbar-item.has-dropdown', 'test_user').click();
cy.get('.navbar-item').last().contains('Log out').click();
});
it('Tries to login with valid credentials but unconfirmed account', () => {
cy.visit('/login', { onBeforeLoad });
cy.get('input[type=email]').type('unconfirmed@email.com');
cy.get('input[type=password]').type('some password');
cy.get('form').submit();
cy.contains('.message.is-danger', 'User with email not found');
});
it('Tries to login with valid credentials, confirmed account but no profile', () => {
cy.visit('/login', { onBeforeLoad });
cy.get('input[type=email]').type('confirmed@email.com');
cy.get('input[type=password]').type('some password');
cy.get('form').submit();
cy.contains('.message', 'To achieve your registration, please create a first identity profile.');
cy.get('form .field').first().contains('label', 'Username').parent().find('input').type('test_user');
cy.get('form .field').eq(2).contains('label', 'Displayed name').parent().find('input').type('Duplicate');
cy.get('form .field').eq(3).contains('label', 'Description').parent().find('textarea').type('This shouln\'t work because it\' using a dupublicated username');
cy.get('.control.has-text-centered').contains('button', 'Create my profile').click();
cy.contains('.help.is-danger', 'Username is already taken');
cy.get('form .field input').first(0).clear().type('test_user_2');
cy.get('form .field input').eq(1).type('Not');
cy.get('form .field textarea').clear().type('This will now work');
cy.get('form').submit();
cy.wait(1000);
cy.contains('.navbar-link', 'test_user_2');
cy.contains('article.message.is-info', 'Welcome back DuplicateNot');
});
}); });

View File

@ -1,12 +1,10 @@
import { onBeforeLoad } from './browser-language'; import { onBeforeLoad } from './browser-language';
beforeEach(() => { beforeEach(() => {
cy.restoreLocalStorage();
cy.checkoutSession(); cy.checkoutSession();
}); });
afterEach(() => { afterEach(() => {
cy.saveLocalStorage();
cy.dropSession(); cy.dropSession();
}); });
@ -34,7 +32,7 @@ describe('Registration', () => {
it('Tests that registration works', () => { it('Tests that registration works', () => {
cy.visit('/register/user', { onBeforeLoad }); cy.visit('/register/user', { onBeforeLoad });
cy.get('input[type=email]').type('user@email.com'); cy.get('input[type=email]').type('user2register@email.com');
cy.get('input[type=password]').type('userPassword'); cy.get('input[type=password]').type('userPassword');
cy.get('form').contains('button.button.is-primary', 'Register').click(); cy.get('form').contains('button.button.is-primary', 'Register').click();
@ -45,7 +43,7 @@ describe('Registration', () => {
cy.get('form .field').eq(3).contains('label', 'Description').parent().find('textarea').type('This is a test account'); cy.get('form .field').eq(3).contains('label', 'Description').parent().find('textarea').type('This is a test account');
cy.get('.control.has-text-centered').contains('button', 'Create my profile').click(); cy.get('.control.has-text-centered').contains('button', 'Create my profile').click();
cy.contains('article.message.is-success', 'Your account is nearly ready, tester').contains('A validation email was sent to user@email.com'); cy.contains('article.message.is-success', 'Your account is nearly ready, tester').contains('A validation email was sent to user2register@email.com');
cy.visit('/sent_emails'); cy.visit('/sent_emails');

View File

@ -24,6 +24,14 @@
// -- This is will overwrite an existing command -- // -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
const AUTH_ACCESS_TOKEN = 'auth-access-token';
const AUTH_REFRESH_TOKEN = 'auth-refresh-token';
const AUTH_USER_ID = 'auth-user-id';
const AUTH_USER_EMAIL = 'auth-user-email';
const AUTH_USER_ACTOR_ID = 'auth-user-actor-id';
const AUTH_USER_ROLE = 'auth-user-role';
let LOCAL_STORAGE_MEMORY = {}; let LOCAL_STORAGE_MEMORY = {};
Cypress.Commands.add("saveLocalStorage", () => { Cypress.Commands.add("saveLocalStorage", () => {
@ -38,6 +46,54 @@ Cypress.Commands.add("restoreLocalStorage", () => {
}); });
}); });
Cypress.Commands.add("clearLocalStorage", () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
localStorage.removeItem(key);
});
});
Cypress.Commands.add("loginUser", () => {
const loginMutation = `
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
accessToken,
refreshToken,
user {
id,
email,
role
}
},
}`;
const body = JSON.stringify({
operationName: 'Login',
query: loginMutation,
variables: { email: 'user@email.com', password: 'some password' }
});
cy.request({
url: 'http://localhost:4000/api',
body: body,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
console.log(res);
const obj = res.body.data.login;
console.log(obj);
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
});
});
Cypress.Commands.add('checkoutSession', async () => { Cypress.Commands.add('checkoutSession', async () => {
const response = await fetch('/sandbox', { const response = await fetch('/sandbox', {
cache: 'no-store', cache: 'no-store',

View File

@ -77,6 +77,7 @@ defmodule MobilizonWeb.Router do
# Because the "/events/:uuid" route caches all these, we need to force them # Because the "/events/:uuid" route caches all these, we need to force them
get("/events/create", PageController, :index) get("/events/create", PageController, :index)
get("/events/list", PageController, :index) get("/events/list", PageController, :index)
get("/events/me", PageController, :index)
get("/events/:uuid/edit", PageController, :index) get("/events/:uuid/edit", PageController, :index)
# This is a hack to ease link generation into emails # This is a hack to ease link generation into emails

57
priv/repo/e2e.seed.exs Normal file
View File

@ -0,0 +1,57 @@
defmodule EndToEndSeed do
alias Mobilizon.Users
def delete_user(email) do
with {:ok, user} <- Users.get_user_by_email(email) do
Users.delete_user(user)
end
end
end
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
if Application.get_env(:mobilizon, :env) != :e2e do
exit(:shutdown)
end
# An user that has just been registered
test_user_unconfirmed = %{email: "unconfirmed@email.com", password: "some password"}
# An user that has registered and has confirmed their account, but no attached identity
test_user_confirmed = %{email: "confirmed@email.com", password: "some password"}
# An user that has registered and has confirmed their account, with a profile
test_user = %{email: "user@email.com", password: "some password"}
test_actor = %{preferred_username: "test_user", name: "I'm a test user", domain: nil}
EndToEndSeed.delete_user(test_user_unconfirmed.email)
EndToEndSeed.delete_user(test_user_confirmed.email)
EndToEndSeed.delete_user(test_user.email)
{:ok, %User{} = _user_unconfirmed} = Users.register(test_user_unconfirmed)
{:ok, %User{} = user_confirmed} = Users.register(test_user_confirmed)
Users.update_user(user_confirmed, %{
"confirmed_at" => Timex.shift(user_confirmed.confirmation_sent_at, hours: -3),
"confirmation_sent_at" => nil,
"confirmation_token" => nil
})
{:ok, %User{} = user} = Users.register(test_user)
Users.update_user(user, %{
"confirmed_at" => Timex.shift(user.confirmation_sent_at, hours: -3),
"confirmation_sent_at" => nil,
"confirmation_token" => nil
})
{:ok, %Actor{}} =
Actors.new_person(%{
user_id: user.id,
preferred_username: test_actor.preferred_username,
name: test_actor.name
})