Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions app/gui/integration-test/dashboard/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type LocalTrackedCalls,
} from './localApi'
import LoginPageActions from './LoginPageActions'
import { passAgreementsDialog, TEXT, type MockParams } from './utilities'
import { passAgreementsDialog, TEXT, VALID_PASSWORD, type MockParams } from './utilities'
export * from './utilities'

export const getText = (key: TextId, ...replacements: Replacements[TextId]) => {
Expand All @@ -28,23 +28,35 @@ export function getAuthFilePath() {
}

/** Perform a successful login. */
async function loginIfNeeded(page: Page, actions: LoginPageActions<Context>) {
async function login({ page }: MockParams, email = '[email protected]', password = VALID_PASSWORD) {
const authFile = getAuthFilePath()

await waitForLoaded(page)
const isLoggedIn = (await page.getByTestId('before-auth-layout').count()) === 0

if (isLoggedIn) {
test.info().annotations.push({
type: 'skip',
description: 'Already logged in',
})
const agreementModalVisible = (await page.locator('#agreements-modal').count()) > 0
if (agreementModalVisible) {
await passAgreementsDialog({ page })
await page.context().storageState({ path: authFile })
}
} else {
await actions.login()
await page.context().storageState({ path: authFile })
return
}

return test.step('Login', async () => {
test.info().annotations.push({
type: 'Login',
description: 'Performing login',
})
await page.getByPlaceholder(TEXT.emailPlaceholder).fill(email)
await page.getByPlaceholder(TEXT.passwordPlaceholder).fill(password)
await page.getByRole('button', { name: TEXT.login, exact: true }).getByText(TEXT.login).click()

await expect(page.getByText(TEXT.loadingAppMessage)).not.toBeVisible()

await passAgreementsDialog({ page })

await page.context().storageState({ path: authFile })
})
}

/** Wait for the page to load. */
Expand Down Expand Up @@ -132,7 +144,7 @@ export function mockAllAndLogin({
const actions = mockAll({ page, setupAPI, setupLocalAPI })

const driveActions = actions
.step('Pass login screen', (page, _ctx, actions) => loginIfNeeded(page, actions))
.step('Login', (page) => login({ page }))
.step('Wait for dashboard to load', waitForDashboardToLoad)
.into(DrivePageActions<Context>)
return goToCloudFirst ? driveActions.goToCategory.cloud() : driveActions
Expand Down
110 changes: 110 additions & 0 deletions app/gui/integration-test/dashboard/mock/react-stripe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/** @file Mock for `@stripe/react-stripe-js` */

import type {
CardElementProps,
ElementsConsumer as StripeElementConsumer,
Elements as StripeElements,
} from '@stripe/react-stripe-js'
import { createContext, useContext, useEffect, useState } from 'react'

/** */
type ElementsContextValue = Parameters<Parameters<typeof StripeElementConsumer>[0]['children']>[0]

const ElementsContext = createContext<ElementsContextValue>(null!)

/** Elements provider for Stripe. */
export function Elements(...[props]: Parameters<typeof StripeElements>) {
const { stripe: stripeRaw, children } = props
const [stripe, setStripe] = useState(stripeRaw && 'then' in stripeRaw ? null : stripeRaw)
const [elements] = useState(() => {
return {
getElement: (type) => {
switch (type) {
case 'card': {
return CardElement
}
default: {
return (<></>) as any
}
}
},
} satisfies Partial<
ElementsContextValue['elements']
> as unknown as ElementsContextValue['elements']
})

useEffect(() => {
let canceled = false
if (stripeRaw && 'then' in stripeRaw) {
void stripeRaw.then((awaitedStripe) => {
if (!canceled) {
setStripe(awaitedStripe)
}
})
}
return () => {
canceled = true
}
}, [stripeRaw])

return (
stripe && (
<ElementsContext.Provider
value={{
stripe,
elements,
}}
>
{children}
</ElementsContext.Provider>
)
)
}

/** Elements consumer for Stripe. */
export function ElementsConsumer(...[props]: Parameters<typeof StripeElementConsumer>) {
return props.children(useContext(ElementsContext))
}

/** Card element for Stripe. */
export function CardElement(props: CardElementProps) {
const { onReady: onReadyRaw, onChange: onChangeRaw } = props
const onReady = onReadyRaw ?? (() => {})
const onChange = onChangeRaw ?? (() => {})

useEffect(() => {
onReady({
blur: () => {},
clear: () => {},
destroy: () => {},
focus: () => {},
mount: () => {},
unmount: () => {},
update: () => {},
on: () => null!,
off: () => null!,
once: () => null!,
})
}, [onReady])

useEffect(() => {
onChange({
elementType: 'card',
empty: false,
complete: true,
error: undefined,
value: { postalCode: '40001' },
brand: 'mastercard',
})
}, [onChange])

return <></>
}

export const useStripe = () => ({
confirmCardSetup: () => {},
})

export const useElements = () => ({
getElement: () => {},
})
26 changes: 26 additions & 0 deletions app/gui/integration-test/dashboard/mock/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @file Mock for `@stripe/stripe-js` */
import type { Stripe } from '@stripe/stripe-js'

export const loadStripe = (): Promise<Stripe> =>
Promise.resolve({
createPaymentMethod: () =>
Promise.resolve({
paymentMethod: {
id: '',
object: 'payment_method',
// eslint-disable-next-line camelcase
billing_details: {
address: null,
email: null,
name: null,
phone: null,
},
created: Number(new Date()) / 1_000,
customer: null,
livemode: true,
metadata: {},
type: '',
},
error: undefined,
}),
} satisfies Partial<Stripe> as Partial<Stripe> as Stripe)
2 changes: 2 additions & 0 deletions app/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
"@react-aria/interactions": "3.23.0",
"@sentry/vite-plugin": "^2.22.7",
"@sentry/vue": "^7.120.2",
"@stripe/react-stripe-js": "^2.9.0",
"@stripe/stripe-js": "^3.5.0",
"@tanstack/react-query": "5.59.20",
"@tanstack/vue-query": "5.59.20",
"@vue/reactivity": "^3.5.13",
Expand Down
4 changes: 1 addition & 3 deletions app/gui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ export default defineConfig({
fullyParallel: true,
...(WORKERS ? { workers: WORKERS } : {}),
forbidOnly: isCI,
// Make test preview use the same port as test URL, so that svg icons are properly displayed.
// Unfortunately we can't make it work for both dashboard and project-view at the same time.
reporter: isCI ? [['list'], ['blob']] : [['html', { port: ports.projectView }]],
reporter: isCI ? [['list'], ['blob']] : [['html']],
retries: isCI ? 1 : 0,
use: {
actionTimeout: 5000,
Expand Down
9 changes: 7 additions & 2 deletions app/gui/src/dashboard/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import type {
TestIdProps,
} from '#/components/types'
import { tv, type VariantProps } from '#/utilities/tailwindVariants'
import icons from '@/assets/icons.svg'
import { isIconName, type Icon as PossibleIcon } from '@/util/iconMetadata/iconName'
import { svgUseHref } from '@/util/icons'
import { memo } from 'react'
import SvgMask from '../SvgMask'

Expand Down Expand Up @@ -171,7 +171,12 @@ export function SvgUse(props: SvgUseProps) {
preserveAspectRatio="xMidYMid slice"
aria-label={alt}
>
<use href={svgUseHref(icon)} className="h-full w-full" aria-hidden="true" data-icon={icon} />
<use
href={icon.includes(':') ? icon : `${icons}#${icon}`}
className="h-full w-full"
aria-hidden="true"
data-icon={icon}
/>
</svg>
)
}
Expand Down
6 changes: 0 additions & 6 deletions app/gui/src/project-view/assets/icon-missing.svg

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
import GrowingSpinner from '@/components/shared/GrowingSpinner.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import { type AnyWidgetIcon } from '@/util/icons'
import { type URLString } from '@/util/data/urlString'
import { type Icon } from '@/util/iconMetadata/iconName'
import { computed } from 'vue'

const props = defineProps(widgetProps(widgetDefinition))
Expand All @@ -16,7 +17,7 @@ export const DisplayIcon: unique symbol = Symbol.for('WidgetInput:DisplayIcon')
declare module '@/providers/widgetRegistry' {
export interface WidgetInput {
[DisplayIcon]?: {
icon: AnyWidgetIcon
icon: Icon | URLString | '$evaluating'
allowChoice?: boolean
showContents?: boolean
noGap?: boolean
Expand Down
5 changes: 3 additions & 2 deletions app/gui/src/project-view/components/StandaloneButton.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { type AnyIcon } from '@/util/icons'
import type { URLString } from '@/util/data/urlString'
import type { Icon } from '@/util/iconMetadata/iconName'
import SvgButton from './SvgButton.vue'

const props = defineProps<{
icon?: AnyIcon | undefined
icon?: Icon | URLString | undefined
label?: string | undefined
disabled?: boolean
title?: string | undefined
Expand Down
5 changes: 3 additions & 2 deletions app/gui/src/project-view/components/SvgButton.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script setup lang="ts">
import MenuButton from '@/components/MenuButton.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { AnyIcon } from '@/util/icons'
import type { URLString } from '@/util/data/urlString'
import type { Icon } from '@/util/iconMetadata/iconName'

const toggledOn = defineModel<boolean | undefined>()
defineProps<{
name?: AnyIcon | undefined
name?: Icon | URLString | undefined
label?: string | undefined
disabled?: boolean | undefined
title?: string | undefined
Expand Down
20 changes: 14 additions & 6 deletions app/gui/src/project-view/components/SvgIcon.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
<script setup lang="ts">
<script lang="ts">
/**
* @file A component displaying a SVG icon.
* A component displaying a SVG icon.
*
* It displays one group defined in `@/assets/icons.svg` file, specified by `variant` property.
*/
import { AnyIcon, svgUseHref } from '@/util/icons'
export default {}
</script>

<script setup lang="ts">
import icons from '@/assets/icons.svg'
import type { URLString } from '@/util/data/urlString'
import type { Icon } from '@/util/iconMetadata/iconName'

const { name } = defineProps<{ name: AnyIcon }>()
const props = defineProps<{
name: Icon | URLString
}>()
</script>

<template>
<svg class="SvgIcon" viewBox="0 0 16 16" preserveAspectRatio="xMidYMid slice">
<use :href="svgUseHref(name)"></use>
<use :href="props.name.includes(':') ? props.name : `${icons}#${props.name}`"></use>
</svg>
</template>

<style scoped>
.SvgIcon {
svg.SvgIcon {
overflow: visible; /* Prevent slight cutting off icons that are using all available space. */
width: var(--icon-width, var(--icon-size, 16px));
height: var(--icon-height, var(--icon-size, 16px));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import icons from '@/assets/icons.svg'
import AgGridTableView, { commonContextMenuActions } from '@/components/shared/AgGridTableView.vue'
import {
useTableVizToolbar,
type SortModel,
} from '@/components/visualizations/TableVisualization/tableVizToolbar'
import { Ast } from '@/util/ast'
import { Pattern } from '@/util/ast/match'
import { svgUseHref } from '@/util/icons'
import { Icon } from '@/util/iconMetadata/iconName'
import { useVisualizationConfig } from '@/util/visualizationBuiltins'
import type {
CellClassParams,
Expand Down Expand Up @@ -182,8 +183,8 @@ const grid = ref<
ComponentInstance<typeof AgGridTableView> & ComponentExposed<typeof AgGridTableView>
>()

const getSvgTemplate = (icon: string) =>
`<svg viewBox="0 0 16 16" width="16" height="16"><use xlink:href="${svgUseHref(icon)}"/></svg>`
const getSvgTemplate = (icon: Icon) =>
`<svg viewBox="0 0 16 16" width="16" height="16"> <use xlink:href="${icons}#${icon}"/> </svg>`

const getContextMenuItems = (
params: GetContextMenuItemsParams,
Expand All @@ -200,7 +201,7 @@ const getContextMenuItems = (
const createMenuItem = ({ name, action, colId, rowIndex, icon }: (typeof actions)[number]) => ({
name,
action: () => createValueNode(colId, rowIndex, action),
icon: getSvgTemplate(icon),
icon: getSvgTemplate(icon as Icon),
})

return [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { AnyIcon } from '@/util/icons'
import type { URLString } from '@/util/data/urlString'
import type { Icon } from '@/util/iconMetadata/iconName'
import type { ToValue } from '@/util/reactivity'
import type { Ref } from 'vue'

export interface Button {
iconStyle?: Record<string, string>
title?: string
dataTestid?: string
icon: AnyIcon
icon: Icon | URLString
}

export interface ActionButton extends Button {
Expand Down
Loading
Loading