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
2 changes: 1 addition & 1 deletion src/components/WithHelpText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const Root = styled('div')((
},

[`& .${classes.notchedChild}`]: {
padding: theme.spacing(1),
padding: theme.spacing(1, 1.75),
},
[`& .${classes.verticalHr}`]: {
display: "flex",
Expand Down
95 changes: 95 additions & 0 deletions src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from "react"
import { render, screen, fireEvent, within } from "@testing-library/react"
import "@testing-library/jest-dom"
import ValidateEvent, { ValidateEventProps } from "."
import { EventCtx, EventPayload } from ".."
import useValidateEvent from "./useValidateEvent"
import { EventType } from "../types"

// Mock the useValidateEvent hook. This allows us to control its output and check if it's called correctly.
jest.mock("./useValidateEvent", () => ({
__esModule: true,
default: jest.fn(),
}))

const mockedUseValidateEvent = useValidateEvent as jest.Mock

// Mock child components that are not relevant to this test.
jest.mock("@/components/PrettyJson", () => () => <div>PrettyJson</div>)
jest.mock("@/components/Spinner", () => () => <div>Spinner</div>)

const mockValidateEventFn = jest.fn()

// A minimal set of props to render the component.
const defaultProps: ValidateEventProps = {
measurement_id: "",
app_instance_id: "",
firebase_app_id: "",
api_secret: "",
client_id: "",
user_id: "",
formatPayload: jest.fn(),
payloadErrors: undefined,
useTextBox: false,
}

// A helper to render the component with context.
const renderComponent = (props: Partial<ValidateEventProps> = {}) => {
// The component relies on EventCtx for some data. This should be a valid
// EventPayload.
const contextValue: EventPayload = {
instanceId: {
measurement_id: "G-12345",
firebase_app_id: "app:12345",
},
eventName: "test_event",
type: EventType.CustomEvent,
parameters: [],
items: [],
userProperties: [],
timestamp_micros: "",
non_personalized_ads: false,
useTextBox: false,
payloadObj: [],
api_secret: "secret123",
clientIds: {},
}

return render(
<EventCtx.Provider value={contextValue}>
<ValidateEvent {...defaultProps} {...props} />
</EventCtx.Provider>
)
}

describe("ValidateEvent EU endpoint functionality", () => {
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()
// Setup the default mock implementation for useValidateEvent to render the initial state.
mockedUseValidateEvent.mockReturnValue({
status: "not-started",
validateEvent: mockValidateEventFn,
})
})

it("should render with the default endpoint and allow switching to the EU endpoint", () => {
renderComponent()

// 1. Check initial state (default endpoint)
expect(screen.getByText("HOST: www.google-analytics.com", { exact: false })).toBeInTheDocument()
expect(screen.queryByText("HOST: region1.google-analytics.com", { exact: false })).not.toBeInTheDocument()
expect(mockedUseValidateEvent).toHaveBeenCalledWith(false)

// 2. Find and interact with the switch
const euSwitch = within(screen.getByTestId("use-eu-endpoint")).getByRole('checkbox')
expect(euSwitch).toHaveProperty('checked', false)
fireEvent.click(euSwitch)

// 3. Check the new state (EU endpoint)
expect(euSwitch).toHaveProperty('checked', true)
expect(screen.getByText("HOST: region1.google-analytics.com", { exact: false })).toBeInTheDocument()
expect(mockedUseValidateEvent).toHaveBeenCalledTimes(2)
expect(mockedUseValidateEvent).toHaveBeenLastCalledWith(true)
})
})
181 changes: 110 additions & 71 deletions src/components/ga4/EventBuilder/ValidateEvent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ import clsx from "classnames"
import useValidateEvent from "./useValidateEvent"
import Loadable from "@/components/Loadable"
import Typography from "@mui/material/Typography"
import Grid from "@mui/material/Grid"
import Switch from "@mui/material/Switch"
import { PAB, PlainButton } from "@/components/Buttons"
import { Check, Warning, Error as ErrorIcon } from "@mui/icons-material"
import PrettyJson from "@/components/PrettyJson"
import usePayload from "./usePayload"
import { ValidationMessage } from "../types"
import Spinner from "@/components/Spinner"
import { EventCtx, Label } from ".."
import { Card } from "@mui/material"
import { Box, Card } from "@mui/material"
import { green, red } from "@mui/material/colors"
import WithHelpText from "@/components/WithHelpText"

const PREFIX = 'ValidateEvent';

Expand All @@ -47,6 +50,7 @@ interface TemplateProps {
sent?: boolean
payloadErrors?: string | undefined
useTextBox?: boolean
useEuEndpoint: boolean
}

export interface ValidateEventProps {
Expand Down Expand Up @@ -166,16 +170,14 @@ const Template: React.FC<TemplateProps> = ({
error,
valid,
payloadErrors,
useTextBox
useTextBox,
useEuEndpoint,
}) => {

const { instanceId, api_secret } = useContext(EventCtx)!
const payload = usePayload()
return (
<Card
className={clsx(classes.form, classes.template)}
data-testid="validate and send"
>
<>
<Typography className={classes.heading} variant="h3">
{headingIcon}
{heading}
Expand Down Expand Up @@ -249,9 +251,9 @@ const Template: React.FC<TemplateProps> = ({
{instanceId.firebase_app_id &&
`&firebase_app_id=${instanceId.firebase_app_id}`}
{instanceId.measurement_id &&
`&measurement_id=${instanceId.measurement_id}`}{" "}
`&measurement_id=${instanceId.measurement_id}`}{" "} <br />
HTTP/1.1 <br />
HOST: www.google-analytics.com <br />
HOST: {useEuEndpoint ? "region1.google-analytics.com" : "www.google-analytics.com"} <br />
Content-Type: application/json
</Typography>

Expand All @@ -265,87 +267,124 @@ const Template: React.FC<TemplateProps> = ({
tooltipText="Copy payload"
/>
</section>
</Card>
</>
)
}

const ValidateEvent: React.FC<ValidateEventProps> = ({formatPayload, payloadErrors, useTextBox}) => {
const request = useValidateEvent()
const [useEuEndpoint, setUseEuEndpoint] = React.useState(false)
const request = useValidateEvent(useEuEndpoint)

return (
<Loadable
request={request}
renderNotStarted={({ validateEvent }) => (
<Template
heading="This event has not been validated"
headingIcon={<Warning />}
body={
<Root>
<Typography>
Update the event using the controls above.
</Typography>
<Typography>
When you're done editing the event, click "Validate Event" to
check if the event is valid.
</Typography>
</Root>
}
validateEvent={ () => {
<Root className={classes.form}>
<Box mb={1} className={clsx(classes.form, classes.template)}>
<WithHelpText
notched
shrink
label="server endpoint"
helpText="The default endpoint is https://www.google-analytics.com. If 'EU' is selected, the https://region1.google-analytics.com endpoint will be used to validate and send events."
>
<Grid component="label" container alignItems="center" spacing={1}>
<Grid item>Default</Grid>
<Grid item>
<Switch
data-testid="use-eu-endpoint"
checked={useEuEndpoint}
onChange={e => setUseEuEndpoint(e.target.checked)}
name="use-eu-endpoint"
color="primary"
/>
</Grid>
<Grid item>EU</Grid>
</Grid>
</WithHelpText>
</Box>
<Card className={clsx(classes.form, classes.template)} data-testid="validate and send">
<Loadable
request={request}
renderNotStarted={({ validateEvent }) => (
<Template
useEuEndpoint={useEuEndpoint}
heading="This event has not been validated"
headingIcon={<Warning />}
body={
<>
<Typography>
Update the event using the controls above.
</Typography>
<Typography>
When you're done editing the event, click "Validate Event" to
check if the event is valid.
</Typography>
</>
}
validateEvent={() => {
if (formatPayload) {
formatPayload()
}

validateEvent()
}
}
/>
)}
renderInProgress={() => (
<Template heading="Validating" body={<Spinner ellipses />} />
)}
renderFailed={({ validationMessages, validateEvent}) => (
<Template
error
headingIcon={<ErrorIcon />}
heading="Event is invalid"
body=""
validateEvent={ () => {
}}
/>
)}
renderInProgress={() => (
<Template
useEuEndpoint={useEuEndpoint}
heading="Validating"
body={<Spinner ellipses />}
/>
)}
renderFailed={({ validationMessages, validateEvent }) => (
<Template
useEuEndpoint={useEuEndpoint}
error
headingIcon={<ErrorIcon />}
heading="Event is invalid"
body=""
validateEvent={() => {
if (formatPayload) {
formatPayload()
}

validateEvent()
}}
validationMessages={validationMessages}
payloadErrors={payloadErrors}
useTextBox={useTextBox}
/>
)}
renderSuccessful={({
sendToGA,
copyPayload,
copySharableLink,
sent,
}) => (
<Template
useEuEndpoint={useEuEndpoint}
sent={sent}
valid
heading="Event is valid"
headingIcon={<Check />}
sendToGA={sendToGA}
copyPayload={copyPayload}
copySharableLink={copySharableLink}
body={
<>
<Typography>
Use the controls below to copy the event payload or share it
with coworkers.
</Typography>
<Typography>
You can also send the event to Google Analytics and watch it in
action in the Real Time view.
</Typography>
</>
}
}
validationMessages={validationMessages}
payloadErrors={payloadErrors}
useTextBox={useTextBox}
/>
)}
renderSuccessful={({ sendToGA, copyPayload, copySharableLink, sent}) => (
<Template
sent={sent}
valid
heading="Event is valid"
headingIcon={<Check />}
sendToGA={sendToGA}
copyPayload={copyPayload}
copySharableLink={copySharableLink}
body={
<>
<Typography>
Use the controls below to copy the event payload or share it
with coworkers.
</Typography>
<Typography>
You can also send the event to Google Analytics and watch it in
action in the Real Time view.
</Typography>
</>
}
/>
)}
/>
)}
/>
</Card>
</Root>
);
}

Expand Down
Loading
Loading