diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79da8400..893ccf61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,3 +101,9 @@ Resulting in a folder structure like the following:
- Continue an incomplete tutorial started in the same workspace. Choose the "continue" path from the start screen. Progress is stored in local storage in the workspace.

+
+## [0.5.0]
+
+- Show error messages in the webview UI
+
+
diff --git a/docs/images/fail-message-in-webview.png b/docs/images/fail-message-in-webview.png
new file mode 100644
index 00000000..c4d5488e
Binary files /dev/null and b/docs/images/fail-message-in-webview.png differ
diff --git a/src/channel/index.ts b/src/channel/index.ts
index 07e4298f..fd7c09cf 100644
--- a/src/channel/index.ts
+++ b/src/channel/index.ts
@@ -14,6 +14,7 @@ import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace'
import { readFile } from 'fs'
import { join } from 'path'
import { promisify } from 'util'
+import { showOutput } from '../services/testRunner/output'
import { WORKSPACE_ROOT } from '../environment'
const readFileAsync = promisify(readFile)
@@ -300,7 +301,9 @@ class Channel implements Channel {
// update progress when a level is deemed complete in the client
await this.context.progress.syncProgress(action.payload.progress)
return
-
+ case 'EDITOR_OPEN_LOGS':
+ const channel = action.payload.channel
+ await showOutput(channel)
default:
logger(`No match for action type: ${actionType}`)
return
diff --git a/src/editor/commands.ts b/src/editor/commands.ts
index 19b96585..002b8211 100644
--- a/src/editor/commands.ts
+++ b/src/editor/commands.ts
@@ -3,7 +3,7 @@ import * as TT from 'typings/tutorial'
import * as vscode from 'vscode'
import createTestRunner from '../services/testRunner'
import { setupActions } from '../actions/setupActions'
-import createWebView from '../webview'
+import createWebView from '../services/webview'
import logger from '../services/logger'
export const COMMANDS = {
@@ -62,9 +62,9 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
// send test pass message back to client
webview.send({ type: 'TEST_PASS', payload: { position } })
},
- onFail: (position: T.Position, message: string) => {
+ onFail: (position: T.Position, failSummary: T.TestFail): void => {
// send test fail message back to client with failure message
- webview.send({ type: 'TEST_FAIL', payload: { position, message } })
+ webview.send({ type: 'TEST_FAIL', payload: { position, fail: failSummary } })
},
onError: (position: T.Position) => {
// TODO: send test error message back to client
diff --git a/src/environment.ts b/src/environment.ts
index 89a61125..6fd41f4d 100644
--- a/src/environment.ts
+++ b/src/environment.ts
@@ -10,7 +10,7 @@ export type Env = 'test' | 'local' | 'development' | 'production'
export const NODE_ENV: Env = process.env.NODE_ENV || 'production'
// toggle logging in development
-export const LOG = false
+export const LOG = true
// error logging tool
export const SENTRY_DSN: string | null = null
diff --git a/src/services/testRunner/formatOutput.ts b/src/services/testRunner/formatOutput.ts
index 00a2d8dc..2f538df4 100644
--- a/src/services/testRunner/formatOutput.ts
+++ b/src/services/testRunner/formatOutput.ts
@@ -4,7 +4,7 @@ import { ParserOutput, Fail } from './parser'
// export const formatSuccessOutput = (tap: ParserOutput): string => {}
export const formatFailOutput = (tap: ParserOutput): string => {
- let output = `FAILED TESTS\n`
+ let output = `FAILED TEST LOG\n`
tap.failed.forEach((fail: Fail) => {
const details = fail.details ? `\n${fail.details}\n` : ''
const logs = fail.logs ? `\n${fail.logs.join('\n')}\n` : ''
diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts
index 182abc5e..57cea72a 100644
--- a/src/services/testRunner/index.ts
+++ b/src/services/testRunner/index.ts
@@ -5,12 +5,12 @@ import logger from '../logger'
import parser from './parser'
import { debounce, throttle } from './throttle'
import onError from '../sentry/onError'
-import { clearOutput, displayOutput } from './output'
+import { clearOutput, addOutput } from './output'
import { formatFailOutput } from './formatOutput'
interface Callbacks {
onSuccess(position: T.Position): void
- onFail(position: T.Position, message: string): void
+ onFail(position: T.Position, failSummary: T.TestFail): void
onRun(position: T.Position): void
onError(position: T.Position): void
}
@@ -51,20 +51,24 @@ const createTestRunner = (config: TT.TutorialTestRunnerConfig, callbacks: Callba
const tap = parser(stdout || '')
- displayOutput({ channel: logChannelName, text: tap.logs.join('\n'), show: false })
+ addOutput({ channel: logChannelName, text: tap.logs.join('\n'), show: false })
if (stderr) {
// FAIL also trigger stderr
if (stdout && stdout.length && !tap.ok) {
- const firstFailMessage = tap.failed[0].message
- callbacks.onFail(position, firstFailMessage)
+ const firstFail = tap.failed[0]
+ const failSummary = {
+ title: firstFail.message || 'Test Failed',
+ description: firstFail.details || 'Unknown error',
+ }
+ callbacks.onFail(position, failSummary)
const output = formatFailOutput(tap)
- displayOutput({ channel: failChannelName, text: output, show: true })
+ addOutput({ channel: failChannelName, text: output, show: true })
return
} else {
callbacks.onError(position)
// open terminal with error string
- displayOutput({ channel: failChannelName, text: stderr, show: true })
+ addOutput({ channel: failChannelName, text: stderr, show: true })
return
}
}
diff --git a/src/services/testRunner/output.ts b/src/services/testRunner/output.ts
index 916b6000..c390c9ba 100644
--- a/src/services/testRunner/output.ts
+++ b/src/services/testRunner/output.ts
@@ -9,22 +9,25 @@ const getOutputChannel = (name: string): vscode.OutputChannel => {
return channels[name]
}
-interface DisplayOutput {
+interface ChannelOutput {
channel: string
text: string
show?: boolean
}
-export const displayOutput = (params: DisplayOutput) => {
+export const addOutput = (params: ChannelOutput) => {
const channel = getOutputChannel(params.channel)
channel.clear()
- channel.show(params.show || false)
channel.append(params.text)
}
+export const showOutput = (channelName: string) => {
+ const channel = getOutputChannel(channelName)
+ channel.show()
+}
+
export const clearOutput = (channelName: string) => {
const channel = getOutputChannel(channelName)
- channel.show(false)
channel.clear()
channel.hide()
}
diff --git a/src/webview/index.ts b/src/services/webview/index.ts
similarity index 98%
rename from src/webview/index.ts
rename to src/services/webview/index.ts
index e6fd4a3e..bfd27ef9 100644
--- a/src/webview/index.ts
+++ b/src/services/webview/index.ts
@@ -1,7 +1,7 @@
import * as path from 'path'
import { Action } from 'typings'
import * as vscode from 'vscode'
-import Channel from '../channel'
+import Channel from '../../channel'
import render from './render'
interface ReactWebViewProps {
diff --git a/src/webview/render.ts b/src/services/webview/render.ts
similarity index 98%
rename from src/webview/render.ts
rename to src/services/webview/render.ts
index 1bf5bf6a..63b2680f 100644
--- a/src/webview/render.ts
+++ b/src/services/webview/render.ts
@@ -1,7 +1,7 @@
import { JSDOM } from 'jsdom'
import * as path from 'path'
import * as vscode from 'vscode'
-import onError from '../services/sentry/onError'
+import onError from '../sentry/onError'
const getNonce = (): string => {
let text = ''
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 78c3d501..a80e8e00 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -42,6 +42,7 @@ export interface TestStatus {
type: 'success' | 'warning' | 'error' | 'loading'
title: string
content?: string
+ timeout?: number
}
export interface MachineContext {
@@ -116,3 +117,8 @@ export interface ProcessEvent {
description: string
status: 'RUNNING' | 'SUCCESS' | 'FAIL' | 'ERROR'
}
+
+export type TestFail = {
+ title: string
+ description: string
+}
diff --git a/web-app/src/components/Message/index.tsx b/web-app/src/components/Message/index.tsx
index c0f428d2..8d3ccc05 100644
--- a/web-app/src/components/Message/index.tsx
+++ b/web-app/src/components/Message/index.tsx
@@ -11,6 +11,7 @@ interface Props {
closeable?: boolean
onClose?: () => void
handleClose?: () => void
+ children?: React.ReactElement | null
}
const Message = (props: Props) => {
@@ -30,7 +31,10 @@ const Message = (props: Props) => {
onClose={onClose}
shape={props.shape}
>
- {props.content}
+ <div>
+ <div>{props.content}</div>
+ <div>{props.children}</div>
+ </div>
</AlifdMessage>
)
}
diff --git a/web-app/src/components/ProcessMessages/TestMessage.tsx b/web-app/src/components/ProcessMessages/TestMessage.tsx
index 57b7eb2e..2eebaa3e 100644
--- a/web-app/src/components/ProcessMessages/TestMessage.tsx
+++ b/web-app/src/components/ProcessMessages/TestMessage.tsx
@@ -5,7 +5,7 @@ import { css, jsx } from '@emotion/core'
const durations = {
success: 1000,
- warning: 4500,
+ warning: 20000,
error: 4500,
loading: 300000,
}
@@ -24,7 +24,7 @@ const useTimeout = ({ duration, key }: { duration: number; key: string }) => {
return timeoutClose
}
-const TestMessage = (props: T.TestStatus) => {
+const TestMessage = (props: T.TestStatus & { children?: React.ReactElement | null }) => {
const duration = durations[props.type]
const timeoutClose = useTimeout({ duration, key: props.title })
return (
@@ -36,7 +36,9 @@ const TestMessage = (props: T.TestStatus) => {
size="medium"
closeable={props.type !== 'loading'}
content={props.content}
- />
+ >
+ {props.children}
+ </Message>
)
}
diff --git a/web-app/src/components/ProcessMessages/index.tsx b/web-app/src/components/ProcessMessages/index.tsx
index c1614c40..80334568 100644
--- a/web-app/src/components/ProcessMessages/index.tsx
+++ b/web-app/src/components/ProcessMessages/index.tsx
@@ -1,12 +1,14 @@
import Message from '../Message'
import * as React from 'react'
import * as T from 'typings'
+import Button from '../Button'
import { css, jsx } from '@emotion/core'
import TestMessage from './TestMessage'
interface Props {
testStatus?: T.TestStatus | null
processes: T.ProcessEvent[]
+ onOpenLogs?: (channel: string) => void
}
const styles = {
@@ -17,9 +19,21 @@ const styles = {
}
// display a list of active processes
-const ProcessMessages = ({ processes, testStatus }: Props) => {
+const ProcessMessages = ({ processes, testStatus, onOpenLogs }: Props) => {
if (testStatus) {
- return <TestMessage {...testStatus} />
+ return (
+ <TestMessage {...testStatus}>
+ {testStatus.type === 'warning' ? (
+ <Button
+ onClick={() => onOpenLogs && onOpenLogs('CodeRoad (Tests)')}
+ type="normal"
+ style={{ marginTop: '0.8rem' }}
+ >
+ Open Logs
+ </Button>
+ ) : null}
+ </TestMessage>
+ )
}
if (!processes.length) {
return null
diff --git a/web-app/src/containers/Tutorial/components/Level.tsx b/web-app/src/containers/Tutorial/components/Level.tsx
index 38f7a8bd..2ee7a26b 100644
--- a/web-app/src/containers/Tutorial/components/Level.tsx
+++ b/web-app/src/containers/Tutorial/components/Level.tsx
@@ -96,6 +96,7 @@ interface Props {
testStatus: T.TestStatus | null
onContinue(): void
onLoadSolution(): void
+ onOpenLogs(channel: string): void
}
const Level = ({
@@ -107,6 +108,7 @@ const Level = ({
status,
onContinue,
onLoadSolution,
+ onOpenLogs,
processes,
testStatus,
}: Props) => {
@@ -170,7 +172,7 @@ const Level = ({
{(testStatus || processes.length > 0) && (
<div css={styles.processes}>
- <ProcessMessages processes={processes} testStatus={testStatus} />
+ <ProcessMessages processes={processes} testStatus={testStatus} onOpenLogs={onOpenLogs} />
</div>
)}
diff --git a/web-app/src/containers/Tutorial/index.tsx b/web-app/src/containers/Tutorial/index.tsx
index 4c04f660..c0dd156f 100644
--- a/web-app/src/containers/Tutorial/index.tsx
+++ b/web-app/src/containers/Tutorial/index.tsx
@@ -32,6 +32,10 @@ const TutorialPage = (props: PageProps) => {
props.send({ type: 'STEP_SOLUTION_LOAD' })
}
+ const onOpenLogs = (channel: string): void => {
+ props.send({ type: 'OPEN_LOGS', payload: { channel } })
+ }
+
const steps = levelData.steps.map((step: TT.Step) => {
// label step status for step component
let status: T.ProgressStatus = 'INCOMPLETE'
@@ -61,6 +65,7 @@ const TutorialPage = (props: PageProps) => {
status={progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE'}
onContinue={onContinue}
onLoadSolution={onLoadSolution}
+ onOpenLogs={onOpenLogs}
processes={processes}
testStatus={testStatus}
/>
diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts
index e3121018..da0f2e97 100644
--- a/web-app/src/services/state/actions/editor.ts
+++ b/web-app/src/services/state/actions/editor.ts
@@ -1,4 +1,4 @@
-import * as CR from 'typings'
+import * as T from 'typings'
import * as TT from 'typings/tutorial'
import * as selectors from '../../selectors'
@@ -8,7 +8,7 @@ export default (editorSend: any) => ({
type: 'EDITOR_STARTUP',
})
},
- configureNewTutorial(context: CR.MachineContext) {
+ configureNewTutorial(context: T.MachineContext) {
editorSend({
type: 'EDITOR_TUTORIAL_CONFIG',
payload: {
@@ -17,7 +17,7 @@ export default (editorSend: any) => ({
},
})
},
- continueConfig(context: CR.MachineContext) {
+ continueConfig(context: T.MachineContext) {
editorSend({
type: 'EDITOR_TUTORIAL_CONTINUE_CONFIG',
payload: {
@@ -26,7 +26,7 @@ export default (editorSend: any) => ({
},
})
},
- loadLevel(context: CR.MachineContext): void {
+ loadLevel(context: T.MachineContext): void {
const level: TT.Level = selectors.currentLevel(context)
const step: TT.Step | null = selectors.currentStep(context)
// load step actions
@@ -41,7 +41,7 @@ export default (editorSend: any) => ({
},
})
},
- loadStep(context: CR.MachineContext): void {
+ loadStep(context: T.MachineContext): void {
const step: TT.Step | null = selectors.currentStep(context)
if (step && step.setup) {
// load step actions
@@ -58,7 +58,7 @@ export default (editorSend: any) => ({
})
}
},
- editorLoadSolution(context: CR.MachineContext): void {
+ editorLoadSolution(context: T.MachineContext): void {
const step: TT.Step | null = selectors.currentStep(context)
// tell editor to load solution commit
if (step && step.solution) {
@@ -74,7 +74,7 @@ export default (editorSend: any) => ({
})
}
},
- syncLevelProgress(context: CR.MachineContext): void {
+ syncLevelProgress(context: T.MachineContext): void {
editorSend({
type: 'EDITOR_SYNC_PROGRESS',
payload: {
@@ -95,4 +95,10 @@ export default (editorSend: any) => ({
type: 'EDITOR_REQUEST_WORKSPACE',
})
},
+ editorOpenLogs(context: T.MachineContext, event: T.MachineEvent): void {
+ editorSend({
+ type: 'EDITOR_OPEN_LOGS',
+ payload: { channel: event.payload.channel },
+ })
+ },
})
diff --git a/web-app/src/services/state/actions/testNotify.ts b/web-app/src/services/state/actions/testNotify.ts
index 5b509adc..c5f7bda3 100644
--- a/web-app/src/services/state/actions/testNotify.ts
+++ b/web-app/src/services/state/actions/testNotify.ts
@@ -20,8 +20,8 @@ const testActions: ActionFunctionMap<CR.MachineContext, CR.MachineEvent> = {
testFail: assign({
testStatus: (context, event) => ({
type: 'warning',
- title: 'Fail!',
- content: event.payload.message,
+ title: event.payload.fail.title,
+ content: event.payload.fail.description,
}),
}),
// @ts-ignore
diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts
index 8b91d813..ccf7589a 100644
--- a/web-app/src/services/state/machine.ts
+++ b/web-app/src/services/state/machine.ts
@@ -170,6 +170,9 @@ export const createMachine = (options: any) => {
STEP_SOLUTION_LOAD: {
actions: ['editorLoadSolution'],
},
+ OPEN_LOGS: {
+ actions: ['editorOpenLogs'],
+ },
},
},
TestRunning: {
diff --git a/web-app/stories/Checkbox.stories.tsx b/web-app/stories/Checkbox.stories.tsx
index c8c935ca..ae1d1431 100644
--- a/web-app/stories/Checkbox.stories.tsx
+++ b/web-app/stories/Checkbox.stories.tsx
@@ -1,6 +1,5 @@
import { storiesOf } from '@storybook/react'
import React from 'react'
-import { css, jsx } from '@emotion/core'
import Checkbox from '../src/components/Checkbox'
import SideBarDecorator from './utils/SideBarDecorator'